import { storage, reflect, string, eventFields } from '@amp-metrics/mt-metricskit-utils-private';
import { loggerNamed } from '@amp-metrics/mt-client-logger-core';

/*
 *  src/system/environment.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 * Provides a set of environment-specific (platform-specific) functions which can be individually overridden for the needs
 * of the particular environment, or replaced en masse by providing a single replacement environment delegate object
 * The functionality in this class is typically replaced via a delegate.
 * @see setDelegate
 * @delegatable
 * @constructor
 */
var Environment = function Environment() {};

/**
 ************************************ PUBLIC METHODS/IVARS ************************************
 */

/**
 * Allows replacement of one or more of this class' functions
 * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method.
 * To replace *all* methods of his class, simply have your delegate implement all the methods of this class
 * Your delegate can be a true object instance, an anonymous object, or a class object.
 * Your delegate is free to have as many additional non-matching methods as it likes.
 * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended.
 *
 * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
 * This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations.
 * If, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
 * then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
 * but override the "appVersion" method again, this time with their own supplied delegate.
 *
 * NOTE: The delegate function will have a property called origFunction representing the original function that it replaced.
 * This allows the delegate to, essentially, call "super" before or after it does some work.
 * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function.
 * @example:
 * To override one or more methods, in place:
 *      eventRecorder.setDelegate({recordEvent: itms.recordEvent});
 * To override one or more methods with a separate object:
 *      eventRecorder.setDelegate(eventRecorderDelegate);
 *      (where "eventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          var eventRecorderDelegate = {recordEvent: itms.recordEvent,
 *                                       sendMethod: 'itms'};
 * To override one or more methods with an instantiated object from a class definition:
 *      eventRecorder.setDelegate(new EventRecorderDelegate());
 *      (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          function EventRecorderDelegate() {
 *         }
 *         EventRecorderDelegate.prototype.recordEvent = itms.recordEvent;
 *         EventRecorderDelegate.prototype.sendMethod = function sendMethod() {
 *                                                          return 'itms';
 *                                                      };
 * To override one or more methods with a class object (with "static" methods):
 *      eventRecorder.setDelegate(EventRecorderDelegate);
 *      (where "EventRecorderDelegate" might be defined elsewhere as, e.g.:
 *          function EventRecorderDelegate() {
 *         }
 *         EventRecorderDelegate.recordEvent = itms.recordEvent;
 *         EventRecorderDelegate.sendMethod = function sendMethod() {
 *                                                return 'itms';
 *                                            };
 * @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods.
 * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object,
 * otherwise returns false.
 */
Environment.prototype.setDelegate = function setDelegate(delegate) {
    return reflect.attachDelegate(this, delegate);
};

/**
 * Some clients have platform-specific implementations of these objects (e.g. iTunes.sessionStorage), so we cover them in case they need to be overriden.
 */
Environment.prototype.localStorageObject = storage.localStorageObject;
Environment.prototype.sessionStorageObject = storage.sessionStorageObject;

/**
 * Fetching identifier entity from AMS Metrics Identifier API
 * @param {String} idNamespace - The id namespace that is defined under 'metricsIdentifier' in the bag
 * @param {'userid' | 'clientid'} idType - The identifier type (userid or clientid)
 * @param {Boolean} crossDeviceSync - The boolean flag to indicate whether the identifier is synced across devices
 * @returns {Promise}
 * @overridable
 */
Environment.prototype.platformIdentifier = function platformIdentifier(idNamespace, idType, crossDeviceSync) {};

/*
 *  src/system/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

var System = function System() {
    this.environment = new Environment();
    this.logger = loggerNamed('mt-client-constraints');
};

/*
 *  src/utils/key_value.js
 *  mt-client-constraints
 *
 *  Copyright © 2023 Apple Inc. All rights reserved.
 *
 */

/**
 * Recursively look up the given keyPath in the provided object.
 * @param {Object} object an object that going to be used for seeking the keyPath
 * @param {String} keyPath a string used to search one or more fields in the object
 * @param {Boolean} inplace a boolean to indicate passing the original/cloned parent to the callback.
 * @param {Function} callback a function will be called when the keyPath has been found in the object
 * @example
 *
 * var target = {
 *     aField: {
 *         bField: {
 *             cArray: [1, 2, 3],
 *             dField: 12345
 *         },
 *         eArray: [{
 *             gField: 'hello'
 *         }, {
 *             gField: 'world'
 *         }]
 *     }
 * }
 *
 * // loop an object field
 * lookForKeyPath(target, 'aField.bField', false, (value, key, keyPath, object) => {
 *     // value = target.aField.bField
 *     // key = 'bField'
 *     // keyPath = 'aField.bField'
 *     // object = target.aField;
 * });
 *
 * // loop an array field
 * lookForKeyPath(target, 'aField.bField.cArray[]', false, (value, key, keyPath, object) => {
 *     // Will be called with 3 times with:
 *     // value = 1, key = 0, keyPath = aField.bField.cArray[0], object = target.aField.bField.cArray
 *     // value = 2, key = 1, keyPath = aField.bField.cArray[1], object = target.aField.bField.cArray
 *     // value = 3, key = 2, keyPath = aField.bField.cArray[2], object = target.aField.bField.cArray
 * });
 *
 * // loop a nested field
 * lookForKeyPath(target, 'aField.bField.dField', false, (value, key, keyPath, object) => {
 *     // value = 12345
 *     // key = 'dField'
 *     // keyPath = 'aField.bField.dField';
 *     // object = target.aField.bField;
 * });
 *
 * // loop a field of an object in an array field
 * lookForKeyPath(target, 'aField.eArray[].gField',false, (value, key, keyPath, object) => {
 *     // Will be called with 2 times with:
 *     // value = hello, key = gField, keyPath = aField.eArray[0].gField, object = aField.eArray[0]
 *     // value = world, key = gField, keyPath = aField.eArray[1].gField, object = aField.eArray[1]
 * });
 */
function lookForKeyPath(object, keyPath, inplace, callback) {
    if (!reflect.isDefined(object) || !reflect.isDefinedNonNullNonEmpty(keyPath) || !reflect.isFunction(callback)) {
        return object;
    }
    var keyPathArray = keyPath.split('.');
    return _lookForKeyPath(object, keyPathArray, null, [], null, inplace, callback);
}

function _lookForKeyPath(object, keyPathArray, key, keyPath, parent, inplace, callback) {
    if (reflect.isFunction(object)) {
        return parent || object;
    }

    keyPath.push(key);

    // Handle the leaf fields
    if (keyPathArray.length === 0) {
        callback(object, key, keyPath.slice(1).join('.'), parent);
        return parent || object;
    }

    if (!reflect.isDefined(object)) {
        return parent || object;
    }

    var clonedObject = inplace ? object : {};
    var fieldName = keyPathArray.shift();
    // Handle array values
    if (fieldName.length > 2 && fieldName.indexOf('[]') === fieldName.length - 2) {
        fieldName = fieldName.slice(0, -2); // remove []
        keyPath.push(fieldName);
        reflect.extend(clonedObject, object);
        var arrayValue = clonedObject[fieldName];
        if (reflect.isDefinedNonNull(arrayValue)) {
            var processedArray = arrayValue.map(function (arrayItem, i) {
                var updatedArray = inplace ? arrayValue : arrayValue.slice();
                _lookForKeyPath(arrayItem, keyPathArray.slice(), i, keyPath, updatedArray, inplace, callback);
                keyPath.pop();
                return updatedArray[i];
            });
            clonedObject[fieldName] = processedArray;
        }
    } else {
        var fieldValue = object[fieldName];
        reflect.extend(clonedObject, object);
        // Handle normal values
        clonedObject = _lookForKeyPath(fieldValue, keyPathArray, fieldName, keyPath, clonedObject, inplace, callback);
    }

    keyPath.pop();

    if (parent) {
        parent[key] = clonedObject;
        return parent;
    } else {
        return clonedObject;
    }
}

/*
 *  src/treatment_matchers/nested_fields_match
 *  mt-client-constraints
 *
 *  Copyright © 2023 Apple Inc. All rights reserved.
 *
 */

var MATCH_TYPES_CONFIG = {
    // "MATCH_TYPES_CONFIG.all" is used for checking if all items of the nested fields meet the filter condition.
    all: {
        initMatchValue: true,
        accumulateMatchResult: function (accumulatedResult, matchResult) {
            return accumulatedResult && matchResult;
        }
    },
    // "MATCH_TYPES_CONFIG.any" is used for checking if any of the items of the nested fields meet the filter condition.
    any: {
        initMatchValue: false,
        accumulateMatchResult: function (accumulatedResult, matchResult) {
            return accumulatedResult || matchResult;
        }
    }
};

function getMatchTypeConfig(matchType) {
    var matchConfig = MATCH_TYPES_CONFIG[matchType];
    if (!reflect.isDefinedNonNull(matchConfig)) {
        matchConfig = MATCH_TYPES_CONFIG.all;
    }
    return matchConfig;
}

/**
 *
 * @param {String} fieldName - name of field in eventData
 * @param {Object} eventData - a dictionary of event data
 * @param {Object} matchOptions - an object contains the configurations that related to nested fields and the match options for the compouned matches.
 * @param {Object} matchOptions.matchType - a flag to indicate the match type to apply to the nested fields. Available values "all", "any"
 * @param {Object} matchOptions.matches - an object contains key/value pairs for the actual matches.
 * @returns {Boolean} return true if the field value exists in "fieldMatchValues" otherwise return false
 */
function nestedFieldCompoundMatch(fieldName, eventData, matchOptions) {
    if (!reflect.isObject(eventData) || !reflect.isObject(matchOptions)) {
        return false;
    }
    var matchType = matchOptions.matchType;
    var compoundMatches = matchOptions.matches;
    if (!reflect.isDefinedNonNullNonEmpty(compoundMatches)) {
        return false;
    }
    var matchTypeConfig = getMatchTypeConfig(matchType);
    var isMatched = matchTypeConfig.initMatchValue;
    lookForKeyPath(eventData, fieldName, false, function (_value, key, _keyPath, object) {
        var matchResult = Object.keys(compoundMatches).every(function (matcherName) {
            var matcherParam = compoundMatches[matcherName];

            if (reflect.isDefinedNonNull(matchers[matcherName])) {
                return matchers[matcherName](key, object, matcherParam);
            } else {
                return false;
            }
        });
        isMatched = matchTypeConfig.accumulateMatchResult(isMatched, matchResult);
    });

    return !!isMatched;
}

/*
 *  src/treatment_matchers/non_empty_match
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

/**
 *
 * @param {String} fieldName - name of field in eventData
 * @param {Object} eventData - a dictionary of event data
 * @returns {Boolean} return true if the fieldName does exist in the eventData otherwise return false
 */
function nonEmptyMatch(fieldName, eventData) {
    // Since the isObject will return undefined/null if the eventData is undefined/null.
    // workaround here to convert the return value to boolean here to ensure this function returns boolean value. Should be fix it in the isObject()
    return (
        !!reflect.isObject(eventData) &&
        eventData.hasOwnProperty(fieldName) &&
        reflect.isDefinedNonNullNonEmpty(eventData[fieldName])
    );
}

/*
 *  src/treatment_matchers/value_match
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

/**
 *
 * @param {String} fieldName - name of field in eventData
 * @param {Object} eventData - a dictionary of event data
 * @param {Array} fieldMatchValues - a list of possible values to match for that field
 * @returns {Boolean} return true if the field value exists in "fieldMatchValues" otherwise return false
 */
function valueMatch(fieldName, eventData, fieldMatchValues) {
    if (!reflect.isObject(eventData)) {
        return false;
    }

    var fieldValue = eventData[fieldName];
    return eventData.hasOwnProperty(fieldName) && fieldMatchValues.indexOf(fieldValue) > -1;
}

/*
 *  src/treatment_matchers/non_value_match
 *  mt-client-constraints
 *
 *  Copyright © 2023 Apple Inc. All rights reserved.
 *
 */

/**
 *
 * @param {String} fieldName - name of field in eventData
 * @param {Object} eventData - a dictionary of event data
 * @param {Array} fieldNotMatchValues - a list of values to not match for that field
 * @returns {Boolean} return true if the field value do not match ALL the values in "fieldNotMatchValues" otherwise return false
 */
function nonValueMatch(fieldName, eventData, fieldNotMatchValues) {
    if (!reflect.isObject(eventData) || !reflect.isArray(fieldNotMatchValues)) {
        return false;
    }

    var fieldValue = eventData[fieldName];
    return eventData.hasOwnProperty(fieldName) && fieldNotMatchValues.indexOf(fieldValue) === -1;
}

/*
 *  src/treatment_matchers/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var matchers = {
    nonEmpty: nonEmptyMatch,
    valueMatches: valueMatch,
    nonValueMatches: nonValueMatch,
    nestedFieldMatches: nestedFieldCompoundMatch
};

/*
 *  src/utils/constants.js
 *  mt-client-constraints
 *
 *  Copyright © 2019 Apple Inc. All rights reserved.
 *
 */

var FIELD_RULES = {
    OVERRIDE_FIELD_VALUE: 'overrideFieldValue'
};

/*
 *  src/field_handlers/base.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 * Provides methods to manage field constraints that apply to all fields
 * @constructor
 */
var Base = function Base() {};

/**
 ************************************ PUBLIC METHODS ************************************
 */
/**
 * @param {Object} eventFields a dictionary of event data
 * @param {Object} fieldRules includes information about how to constrain a field
 * @param {String} fieldName the name of the field to constrain
 * @return {any} a field value that adheres to the provided rules
 * @overridable
 */
Base.prototype.constrainedValue = function constrainedValue(eventFields, fieldRules, fieldName) {
    var fieldValue = eventFields && eventFields.hasOwnProperty(fieldName) ? eventFields[fieldName] : null;
    return this.applyConstraintRules(fieldValue, fieldRules);
};

/**
 * @param {any} fieldValue an unconstrained value
 * @param {Object} fieldRules includes information about how to constrain a field
 * @return {any} a field value that adheres to the provided rules
 * @overridable
 */
Base.prototype.applyConstraintRules = function applyConstraintRules(fieldValue, fieldRules) {
    var returnValue = fieldValue;
    if (fieldRules) {
        var denylisted = fieldRules.denylisted || fieldRules.blacklisted;
        if (denylisted) {
            returnValue = null;
        } else if (fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)) {
            returnValue = fieldRules.overrideFieldValue;
        }
    }
    return returnValue;
};

/*
 *  src/field_actions/base.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */
var exceptionString = string.exceptionString;

/**
 * Parent class of field_actions classes
 * @param {Object} constraintsInstance - the instance of Constraints class
 * @constructor
 */
var Base$1 = function Base(constraintsInstance) {
    // @private
    this._constraintsInstance = constraintsInstance;
};

/**
 ************************************ PUBLIC METHODS/IVARS ************************************
 */

/**
 * Allows replacement of one or more of this class' functions
 * Any method on the passed-in object which matches a method that this class has will be called instead of the built-in class method.
 * To replace *all* methods of his class, simply have your delegate implement all the methods of this class
 * Your delegate can be a true object instance, an anonymous object, or a class object.
 * Your delegate is free to have as many additional non-matching methods as it likes.
 * It can even act as a delegate for multiple MetricsKit objects, though that is not recommended.
 *
 * "setDelegate()" may be called repeatedly, with the functions in the most-recently set delegates replacing any functions matching those in the earlier delegates, as well as any as-yet unreplaced functions.
 * This allows callers to use "canned" delegates to get most of their functionality, but still replace some number of methods that need custom implementations.
 * If, for example, a client wants to use the "canned" itml/environment delegate with the exception of, say, the "appVersion" method, they can set itml/environment as the delegate, and
 * then call "setDelegate()" again with their own delegate containing only a single method of "appVersion" as the delegate, which would leave all the other "replaced" methods intact,
 * but override the "appVersion" method again, this time with their own supplied delegate.
 *
 * NOTE: The delegate function will have a property called origFunction representing the original function that it replaced.
 * This allows the delegate to, essentially, call "super" before or after it does some work.
 * If a replaced method is overridden again with a subsequent "setDelegate()" call, the "origFunction" property will be the previous delegate's function.
 * @param {Object} Object or Class with delegate method(s) to be called instead of default (built-in) methods.
 * @returns {Boolean} true if one or more methods on the delegate object match one or more methods on the default object,
 * otherwise returns false.
 */
Base$1.prototype.setDelegate = function setDelegate(delegate) {
    return reflect.attachDelegate(this, delegate);
};

/**
 * Abstract method to constrain value
 * @param {Any} value - the value of fieldName in eventData, null if the eventData does not exist or the fieldName does not exist in the eventData
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
 * @param {String} fieldName - field name/path that can locate the "value" parameter in eventData
 * @return {Any} the constrained value
 */
Base$1.prototype.constrainedValue = function constrainedValue(value, fieldRules, eventData, fieldName) {
    throw exceptionString('field_actions.Base', 'constrainedValue');
};

/**
 * A public method to wrap the "constrainedValue" method to contains common code for all field_actions subclasses.
 * @param {Any} value - field value in eventData that is performed by field_actions
 * @param {String} fieldName - field name/path that can locate the "value" parameter in eventData
 * @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @return {Any} the constrained value
 */
Base$1.prototype.performAction = function performAction(value, fieldName, eventData, fieldRules) {
    // return the original value if there are no rules to apply
    if (reflect.isDefinedNonNull(fieldRules) && !reflect.isEmptyObject(fieldRules)) {
        value = this.constrainedValue(value, fieldRules, eventData, fieldName);
    }
    return value;
};

/*
 *  src/utils/url.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 ************************************ PUBLIC METHODS/IVARS ************************************
 */

/**
 * @param {String} aUrl
 * @return {String} the hostname part of the provided url e.g. www.apple.com
 * @overridable
 * Note: see https://nodejs.org/api/url.html for a diagram illustrating the different parts of a URL
 */
function hostname(aUrl) {
    aUrl = aUrl || '';

    var urlParts = withoutParams(aUrl).split('/');
    var hostAndAuth;
    var host;
    var hostname;

    if (aUrl.indexOf('//') === -1) {
        hostAndAuth = urlParts[0];
    } else {
        hostAndAuth = urlParts[2];
    }

    host = hostAndAuth.substring(hostAndAuth.indexOf('@') + 1);
    hostname = host.split(':')[0];

    return hostname;
}

/**
 * @param {String} aUrl
 * @return {String} the domain part of the provided url shortened to the "main" part of the domain (apple.com or apple.co.uk)
 * @overridable
 * Note: this method uses a heuristic to determine if the url has a country code second level domain and will miss
 *       ccSLDs that are not exactly two chracters long or one of: 'com', 'org, 'net', edu', 'gov'
 *       For example, "www.example.ltd.uk" will be shortened to "ltd.uk"
 *       All two-letter top-level domains are ccTLDs: https://en.wikipedia.org/wiki/Country_code_top-level_domain
 */
function mainDomain(aUrl) {
    var urlSegments = hostname(aUrl).split('.');
    var lastSegment = urlSegments[urlSegments.length - 1];
    var secondToLastSegment = urlSegments[urlSegments.length - 2];
    var segmentsToKeep = 2;

    if (
        lastSegment &&
        lastSegment.length === 2 &&
        secondToLastSegment &&
        (secondToLastSegment.length === 2 || secondToLastSegment in reservedCCSLDs())
    ) {
        segmentsToKeep = 3;
    }

    return urlSegments.slice(-1 * segmentsToKeep).join('.');
}

/**
 * @return {Object} a map of country-code second level domains (ccSLDs) used in the heuristic
 * that determines the main part of a domain (defined as TLD + ccSLD + 1)
 * @overridable
 */
function reservedCCSLDs() {
    var reservedCCSLDs = {
        com: true,
        org: true,
        net: true,
        edu: true,
        gov: true
    };

    return reservedCCSLDs;
}

/**
 * @param {String} aUrl
 * @param {Array|Object} allowedParams An array of allowed params or an object with each param containing its allowed values
 * @return {String} the url with any disallowed query parameters and/or hash removed
 * @overridable
 *
 * @example
 * withoutParams('https://itunes.apple.com/?p1=10&p2=hello', ['p1']);
 * // returns 'https://itunes.apple.com/?p1=10'
 *
 * withoutParams('https://itunes.apple.com/?p1=10&p2=hello', {
 *   p1: {
 *     allowedValues: ['20', '30']
 *   },
 * . p2: {
 *     allowedValues: ['hello']
 *   }
 * });
 * // returns 'https://itunes.apple.com/?p2=hello'
 */
function withoutParams(aUrl, allowedParams) {
    var url = aUrl || '';
    var urlParts = url.split('?');
    var urlPrefix = urlParts[0];
    var urlParams = withoutHash(urlParts[1]).split('&');

    var filteredParams = urlParams
        .filter(function (paramString) {
            var keyAndVal = paramString.split('=');
            var paramName = keyAndVal[0];
            var paramVal = keyAndVal[1];

            if (reflect.isArray(allowedParams)) {
                return allowedParams.indexOf(paramName) !== -1;
            }

            if (reflect.isObject(allowedParams)) {
                return (
                    reflect.isObject(allowedParams[paramName]) &&
                    reflect.isArray(allowedParams[paramName].allowedValues) &&
                    allowedParams[paramName].allowedValues.indexOf(paramVal) !== -1
                );
            }

            return false;
        })
        .join('&');

    return filteredParams.length > 0 ? urlPrefix + '?' + filteredParams : urlPrefix;
}

/**
 * Returns the url with the hash removed
 * @param {String} aUrl
 * @return {String} the url with any hash removed
 * @overridable
 * @example
 * withoutHash('https://itunes.apple.com:80/music?param1=abc&param2=def&param3=ghi#someHash')
 * // returns 'https://itunes.apple.com:80/music?param1=abc&param2=def&param3=ghi'
 */
function withoutHash(aUrl) {
    var url = aUrl || '';
    return url.split('#')[0];
}

/**
 * Returns the url with all of the string replacements applied
 * @param {String} aUrl
 * @param {Object} replacements The list of replacements that should be applied on the url
 * @param {String} replacements.searchPattern A stringified regex pattern to search for
 * @param {String} replacements.replaceVal The string to replace the match with
 * @param {String} replacements.flags Regex flags to include with the search pattern (ex: 'g')
 * @return {String} The url with all replacements applied
 * @overridable
 *
 * @example
 * withReplacements('https://itunes.apple.com:80/music?param1=abc&param2=def&param3=ghi#someHash', [
 *     {
 *         searchPattern: 'music',
 *         replaceVal: 'm'
 *     }
 * ])
 * // returns 'https://itunes.apple.com:80/m?param1=abc&param2=def&param3=ghi#someHash'
 *
 *  withReplacements('https://apple.com/1234', [
 *     {
 *         searchPattern: '\d',
 *         replaceVal: 'X',
 *         flags: 'g'
 *     }
 * ])
 * // returns 'https://apple.com/XXXX'
 */
function withReplacements(aUrl, replacements) {
    var url = aUrl || '';
    var urlReplacements = replacements || [];

    var replacedUrl = urlReplacements.reduce(function (url, replacement) {
        var searchPattern = new RegExp(replacement.searchPattern, replacement.flags);
        var replaceVal = replacement.replaceVal;
        return url.replace(searchPattern, replaceVal);
    }, url);

    return replacedUrl;
}

/*
 *  src/utils/id_generator.js
 *  mt-client-constraints
 *
 *  Copyright © 2023 Apple Inc. All rights reserved.
 *
 */

var ID_SEPARATOR = '-';
var DEFAULT_GENERATED_ID_SEPARATOR = 'z';

/**
 * @param {Object} options includes - information about how to generate an ID
 * @param {Number} options.idVersion - the version of the ID
 * @param {Number} options.time - the time to be a part of the ID (optional)
 * @param {String} options.generatedIdSeparator - a token-separated hex string of metadata to attach to a ID  (optional) default to "z"
 * @return {String} a generated ID
 */
function generateId(options) {
    if (!reflect.isDefinedNonNull(options) || !reflect.isInteger(options.idVersion)) {
        return '0';
    }
    var uuid = string.uuid();
    var generatedIdSeparator = options.generatedIdSeparator || DEFAULT_GENERATED_ID_SEPARATOR;
    var idString = generatedIdMetadata(options) + ID_SEPARATOR + uuid || '';

    var convertedIdString = idString
        .split(ID_SEPARATOR)
        .map(function (segment) {
            var segmentAsNumber = parseInt(segment, 16);
            return string.convertNumberToBaseAlphabet(segmentAsNumber, string.base61Alphabet);
        })
        .join(generatedIdSeparator);

    return convertedIdString;
}

/**
 * @param {Object} options includes - information about how to generate an ID
 * @param {Number} options.idVersion - the version of the ID
 * @param {Number} options.time - the time to be a part of the ID (optional)
 * @return {String} a token-separated hex string of metadata to attach to a ID,
 */
function generatedIdMetadata(options) {
    var parameters = [options.idVersion];

    if (options.time) {
        parameters.push(options.time);
    }

    return parameters
        .map(function (param) {
            return param.toString(16);
        })
        .join(ID_SEPARATOR);
}

/*
 *  src/field_actions/id_action/time_based_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2021 Apple Inc. All rights reserved.
 *
 */

/**
 * The time-based ID generating strategy
 * The ID value will change after its lifespan expires
 */
function constrainedValue(idString, idRules, eventData, fieldName) {
    var storageKey = this.storageKey(fieldName, eventData, idRules);
    var environment = this._constraintsInstance.system.environment;
    var idData = storage.objectFromStorage(environment.localStorageObject(), storageKey) || {};

    idData.value = this.idString(idData, idRules);
    if (
        this.rulesHaveLifespan(idRules) &&
        (!reflect.isNumber(idData.expirationTime) || this.timeExpired(idData.expirationTime))
    ) {
        idData.expirationTime = this.expirationTime(idRules.lifespan);
    }

    storage.saveObjectToStorage(environment.localStorageObject(), storageKey, idData);
    idString = idData.value;

    return idString;
}

/*
 *  src/field_actions/id_action/session_time_based_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2021 Apple Inc. All rights reserved.
 *
 */

// @private
// A global cache storage to store the served clientIds by the clientId storageKey
// Make it as a global variable to ensure the cached clientId can be shared between multiple MK instances
var _sessionIdCache = {};

/**
 * The user-session-based + time-based ID generating strategy
 * When the id getting expired, this function will return a consistent ID until the current user session ends, even if the ID is scheduled to expire in the middle of the session
 */
function constrainedValue$1(idString, idRules, eventData, fieldName) {
    var storageKey = this.storageKey(fieldName, eventData, idRules);
    var returnedIdString = _sessionIdCache[storageKey];

    if (!returnedIdString) {
        returnedIdString = constrainedValue.apply(this, arguments);
        _sessionIdCache[storageKey] = returnedIdString;
    }
    return returnedIdString;
}

/*
 *  src/field_actions/id_action/id_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var STORAGE_KEY_SEPARATOR = '_';
var MT_ID_NAMESPACE = 'mtId';

var IdAction = function IdAction() {
    Base$1.apply(this, arguments);
};

IdAction.prototype = Object.create(Base$1.prototype);
IdAction.prototype.constructor = IdAction;

/*
 * Possible strategies that can be used to scope an ID value
 */
IdAction.prototype.SCOPE_STRATEGIES = {
    ALL: 'all',
    MAIN_DOMAIN: 'mainDomain'
};

/**
 * @param {Object} (optional) idRules includes information about when to expire the ID
 * @return {Boolean}
 */
IdAction.prototype.rulesHaveLifespan = function rulesHaveLifespan(idRules) {
    idRules = idRules || {};

    return reflect.isNumber(idRules.lifespan);
};

/**
 * @param {Number} (optional) lifespan the amount of time, in milliseconds, that an ID should be valid for
 * @return {Number} a timestamp in ms since epoch, or null if no lifespan was provided
 */
IdAction.prototype.expirationTime = function expirationTime(lifespan) {
    return lifespan ? Date.now() + lifespan : null;
};

/**
 * @param {String} fieldName - name of the field being field_actions in eventData
 * @param {Object} eventData a dictionary of event data
 * @param {Object} idRules includes information about how to namespace/scope the id
 * @return {String} the key that id data should be stored under
 * @example
 * (storageKeyPrefix ? storageKeyPrefix : mtId_<fieldName>)_<namespace>_(scopeStrategy ? <eventData[scopeFieldName]> : 'all')
 * @overridable
 */
IdAction.prototype.storageKey = function storageKey(fieldName, eventData, idRules) {
    var scope = this.scope(eventData, idRules);
    return this.storageKeyPrefix(idRules, fieldName) + (scope ? STORAGE_KEY_SEPARATOR + scope : '');
};

/**
 * @param {Object} idRules includes information about how to namespace/scope the id
 * @param {String} fieldName - name of the field being field_actions in eventData
 * @return {String} a prefix to be used when storing id data in localStorage
 * @overridable
 */
IdAction.prototype.storageKeyPrefix = function storageKeyPrefix(idRules, fieldName) {
    return idRules && reflect.isString(idRules.storageKeyPrefix) && idRules.storageKeyPrefix.length > 0
        ? idRules.storageKeyPrefix
        : MT_ID_NAMESPACE + STORAGE_KEY_SEPARATOR + fieldName;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} idRules includes information about how to namespace/scope the id
 * @return {String} the namespace/scope for this set of event data and rules
 * @overridable
 */
IdAction.prototype.scope = function scope(eventData, idRules) {
    var idKey = '';

    if (idRules) {
        if (idRules.namespace) {
            idKey += idRules.namespace;
        }
        if (idRules.scopeStrategy) {
            var domainScope;

            switch (idRules.scopeStrategy) {
                case this.SCOPE_STRATEGIES.MAIN_DOMAIN:
                    var scopeFieldName = idRules.scopeFieldName;
                    domainScope = mainDomain(eventData[scopeFieldName]) || 'unknownDomain';
                    break;
                case this.SCOPE_STRATEGIES.ALL: /* fall through */
                default:
                    // no scope
                    domainScope = this.SCOPE_STRATEGIES.ALL;
                    break;
            }

            if (idKey.length) {
                idKey += STORAGE_KEY_SEPARATOR;
            }
            idKey += domainScope;
        }
    }

    return idKey;
};

/**
 * @param {Object} (optional) existingIdData
 * @param {Object} (optional) idRules includes information about when to expire the ID
 * @return {String} an ID
 * @overridable
 */
IdAction.prototype.idString = function idString(existingIdData, idRules) {
    var existingId = existingIdData ? existingIdData.value : null;
    var returnValue = existingId;

    if (
        !existingId ||
        (reflect.isNumber(existingIdData.expirationTime) && this.timeExpired(existingIdData.expirationTime))
    ) {
        returnValue = this.generateId(idRules);
    }

    return returnValue;
};

/**
 * @param {Object} (optional) idRules includes information about how to constrain the field
 * @return {String} a generated ID
 * @overridable
 * @see comments in the related MTClientId.java
 */
IdAction.prototype.generateId = function generateId$1(idRules) {
    idRules = idRules || {};
    return generateId({
        idVersion: this.generatedIdVersion(),
        time: this.expirationTime(idRules.lifespan),
        generatedIdSeparator: this.generatedIdSeparator(idRules.tokenSeparator)
    });
};

/**
 * @return {Number} the version of the generated ID
 * @overridable
 */
IdAction.prototype.generatedIdVersion = function generatedIdVersion() {
    return 4;
};

/**
 * @return {String} the separator used to tokenize sections of an unformatted ID string
 * @overridable
 */
IdAction.prototype.idTokenSeparator = function idTokenSeparator() {
    return '-';
};

/**
 * @param {String} (optional) separator
 * @return {String} the separator used to tokenize sections of a finalized, formatted ID string
 * @overridable
 */
IdAction.prototype.generatedIdSeparator = function generatedIdSeparator(separator) {
    return separator || 'z';
};

/**
 * @param {Number} timestamp a timestamp in ms since epoch
 * @return {Boolean}
 * @overridable
 */
IdAction.prototype.timeExpired = function timeExpired(timestamp) {
    return timestamp <= Date.now();
};

/**
 * @param {String} idString - the ID field in eventData
 * @param {Object} idRules - includes information about how to constrain the field
 * @param {String}(optional) idRules.storageKeyPrefix - a prefix to be used when storing ID data in localStorage, default is MT_ID_NAMESPACE
 * @param {String}(optional) idRules.namespace - a string to be used when storing ID data in localStorage.
 * @param {String}(optional) idRules.scopeStrategy - a strategy that can be used to scope a ID value [all/mainDomain]
 * @param {String}(optional) idRules.scopeFieldName - name of the scope field in eventData, the value would be an URL and used to get the main domain as a part of scope. It is used when parameters.scopeStrategy set to "mainDomain"
 * @param {String}(optional) idRules.tokenSeparator - the separator used to tokenize sections of a finalized, formatted ID string. Default is 'z'
 * @param {Integer}(optional) idRules.lifespan - the expiration period for the ID (milliseconds)
 * @param {Boolean}(optional) idRules.persistIdForSession - a boolean to indicate whether to persist the ID until the current user session ends, even if it is scheduled to expire in the middle of the session
 * @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) ID field
 * @param {String} fieldName - name of the field being field_actions in eventData
 * @return {String} the constrained ID
 */
IdAction.prototype.constrainedValue = function constrainedValue$2(idString, idRules, eventData, fieldName) {
    if (eventData && idRules && !reflect.isEmptyObject(idRules)) {
        if (idRules.persistIdForSession === true) {
            idString = constrainedValue$1.apply(this, arguments);
        } else {
            idString = constrainedValue.apply(this, arguments);
        }
    }
    return idString;
};

/*
 *  src/field_handlers/client_id.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 * Provides methods to manage clientId field constraints.
 * @constructor
 */
var ClientId = function ClientId(base, constraintsInstance) {
    // @private
    this._base = base;
    // @private
    this._idAction = new IdAction(constraintsInstance);
    this._idAction.setDelegate({
        storageKey: function storageKey(fieldName, eventData, idRules) {
            return this.storageKeyPrefix() + '_' + this.scope(eventData, idRules);
        }.bind(this._idAction),
        storageKeyPrefix: function storageKeyPrefix() {
            return 'mtClientId';
        }
    });
};

/**
 ************************************ PUBLIC METHODS/IVARS ************************************
 */

/**
 * @param {Object} eventFields a dictionary of event data, which may include a pre-existing (unconstrained) clientId
 * @param {Object} clientIdRules includes information about when to expire the clientId and how to namespace/scope it
 * @return {String} a clientId that adheres to the provided rules
 * @overridable
 */
ClientId.prototype.constrainedValue = function constrainedValue(eventFields, clientIdRules) {
    // adapt expirationPeriod to lifespan
    var clonedRules = clientIdRules;
    if (clientIdRules && reflect.isNumber(clientIdRules.expirationPeriod)) {
        clonedRules = reflect.extend({}, clientIdRules);
        clonedRules.lifespan = clonedRules.expirationPeriod;
        delete clonedRules.expirationPeriod;
    }
    var clientId = eventFields ? eventFields.clientId : null;
    var clientIdString = this._idAction.performAction(clientId, 'clientId', eventFields, clonedRules);

    return this._base.applyConstraintRules(clientIdString, clientIdRules);
};

/*
 *  src/field_actions/url_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var UrlAction = function UrlAction() {
    Base$1.apply(this, arguments);
};

UrlAction.prototype = Object.create(Base$1.prototype);
UrlAction.prototype.constructor = UrlAction;

/*
 * Possible truncation strategies that can be applied to a parentPageUrl
 */
UrlAction.prototype.SCOPES = {
    HOSTNAME: 'hostname',
    FULL: 'full',
    FULL_WITHOUT_PARAMS: 'fullWithoutParams',
    FULL_WITH_REPLACEMENTS: 'fullWithReplacements'
};

/**
 * @param {String} url - the URL field in eventData
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @return {String} the constrained URL
 */
UrlAction.prototype.constrainedValue = function constrainedValue(url, fieldRules) {
    if (url && fieldRules && fieldRules.scope) {
        switch (fieldRules.scope) {
            case this.SCOPES.HOSTNAME:
                url = hostname(url);
                break;
            case this.SCOPES.FULL_WITHOUT_PARAMS:
                url = withoutParams(url, fieldRules.allowedParams);
                break;
            case this.SCOPES.FULL_WITH_REPLACEMENTS:
                url = withReplacements(url, fieldRules.replacements);
                break;
            case this.SCOPES.FULL: /* fall through */
        }
    }

    return url;
};

/*
 *  src/field_handlers/parent_page_url.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 * Provides methods to manage parentPageUrl field constraints.
 * @constructor
 */
var ParentPageUrl = function ParentPageUrl(base) {
    // @private
    this._base = base;
    // @private
    this._urlAction = new UrlAction();
};

/**
 ************************************ PUBLIC METHODS/IVARS ************************************
 */

/**
 * @param {Object} eventFields a dictionary of event data, which should include a pre-existing (unconstrained) parentPageUrl
 * @param {Object} parentPageUrlRules includes information about whether to strip certain parts of the URL
 * @return {String} a parentPageUrl modified according to the provided rules
 * @overridable
 */
ParentPageUrl.prototype.constrainedValue = function constrainedValue(eventFields, parentPageUrlRules) {
    var parentPageUrl = eventFields ? eventFields.parentPageUrl : null;
    var modifiedUrl = this._urlAction.performAction(parentPageUrl, 'parentPageUrl', eventFields, parentPageUrlRules);
    return this._base.applyConstraintRules(modifiedUrl, parentPageUrlRules);
};

/*
 *  src/field_handlers/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

/**
 * @deprecated the field handlers has been deprecated and replaced with "De-res treatments(src/field_actions/*)"
 * @param constraintsInstance
 * @constructor
 */
var FieldHandlers = function (constraintsInstance) {
    this.base = new Base(constraintsInstance);
    this.clientId = new ClientId(this.base, constraintsInstance);
    this.parentPageUrl = new ParentPageUrl(this.base, constraintsInstance);
};

/*
 *  src/treatment/legacy_treatment.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var LegacyTreatment = function LegacyTreatment(constraintInstance) {
    // @private
    this._fieldHandlers = new FieldHandlers(constraintInstance);
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} eventConstraints a set of constraints to apply to this event
 * @return {Object} the event data modified according to the appropriate constraints
 * Note: event fields will be modified in place and also returned
 * @example
 * var eventData = {
 *     eventType: 'click',
 *     pageType: 'TopCharts',
 *     parentPageUrl: 'https://itunes.apple.com/music/topcharts/12345',
 *     // etc.
 * };
 * var eventConstraints = {
 *         fieldConstraints: { parentPageUrl: { scope: 'hostname' } }
 *     }
 * legacyTreatment.applyConstraints(eventData, eventConstraints) =>
 *     {
 *         eventType: click,
 *         pageType: 'TopCharts',
 *         parentPageUrl: 'itunes.apple.com', // truncated to hostname only
 *         etc... // all other fields remain the same
 *     }
 */
LegacyTreatment.prototype.applyConstraints = function applyConstraints(eventData, eventConstraints) {
    if (eventConstraints && eventConstraints.fieldConstraints) {
        eventData = this.applyFieldConstraints(eventData, eventConstraints.fieldConstraints);
    }

    return eventData;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} fieldConstraints a set of constraints to apply to fields in this event, keyed by field name
 * @return {Object} the event data modified according to the appropriate constraints
 * Note: event fields will be modified in place and also returned
 */
LegacyTreatment.prototype.applyFieldConstraints = function applyFieldConstraints(eventData, fieldConstraints) {
    if (fieldConstraints) {
        var constrainedFieldValues = {};
        var constrainedValue;
        var fieldRules;
        var fieldName;

        for (fieldName in fieldConstraints) {
            fieldRules = fieldConstraints[fieldName];
            if (
                eventData.hasOwnProperty(fieldName) ||
                fieldRules.generateValue === true ||
                fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)
            ) {
                if (fieldName in this._fieldHandlers) {
                    constrainedValue = this._fieldHandlers[fieldName].constrainedValue(eventData, fieldRules);
                } else {
                    constrainedValue = this._fieldHandlers.base.constrainedValue(eventData, fieldRules, fieldName);
                }

                constrainedFieldValues[fieldName] = constrainedValue;
            }
        }

        // assign constrained values only after all of them have been calculated
        // in case some constrained values depend on more than one original field values
        for (fieldName in constrainedFieldValues) {
            eventData[fieldName] = constrainedFieldValues[fieldName];
        }

        eventData = eventFields.mergeAndCleanEventFields(eventData);
    }

    return eventData;
};

/*
 *  src/utils/constraint_generator
 *  mt-client-constraints
 *
 *  Copyright © 2021 Apple Inc. All rights reserved.
 *
 */

/**
 * Adding/replacing the rule properties of targetRules with the ones of newRules
 * @param {Object} targetRules - an object contains an <fieldConstraintsName> property with field rules in it.
 * @param {Object} newRules - an object contains an <fieldConstraintsName> property with new field rules in it.
 * @param {String} fieldRulesKeyName - the field rule property name
 * @param {Function} initialFieldRules - a callback function to decide the target field rule object. Function signature: function (targetRules, sourceRules, fieldName)
 * @returns {Object} updated target rules, all replacements are in-place updating unless the passed-in targetRules object does not exist
 */
function updateFieldRulesets(targetRules, newRules, fieldRulesKeyName, initialFieldRules) {
    var updatedRules = targetRules || {};
    initialFieldRules =
        initialFieldRules ||
        function (targetRules, sourceRules, fieldName) {
            return targetRules[fieldName] || {};
        };

    if (newRules && newRules[fieldRulesKeyName]) {
        var fieldName;
        var propertyName;
        var updatedFieldRules = updatedRules[fieldRulesKeyName] || {};
        updatedRules[fieldRulesKeyName] = updatedFieldRules;

        // copying the top level rules over is sufficient for a simple rule structure
        for (fieldName in newRules[fieldRulesKeyName]) {
            var fieldRules = initialFieldRules(updatedFieldRules, newRules[fieldRulesKeyName], fieldName);
            updatedFieldRules[fieldName] = fieldRules;
            for (propertyName in newRules[fieldRulesKeyName][fieldName]) {
                fieldRules[propertyName] = newRules[fieldRulesKeyName][fieldName][propertyName];
            }
        }
    }

    return updatedRules;
}

/*
 *  src/constraint_generator/legacy_constraint_generator.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

var LegacyConstraintGenerator = function LegacyConstraintGenerator(constraintInstance) {
    this.treatment = new LegacyTreatment(constraintInstance);
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} topicConfig the AMP Metrics topic config to use to look up constraint_generator values
 * @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
 * @return {Object} a set of constraints to apply to this event
 * @overridable
 * Constraint rules will be applied in the order they are provided in config.
 * @example
 *     Given the following config:
 *     { constraints: {
 *          defaultProfile: 'strict',
 *          profiles: {
 *              strict: {
 *                  precedenceOrderedRules: [
 *                      {
 *                          filters: 'any',
 *                          fieldConstraints: {
 *                              clientId: {
 *                                  generateValue: true,
 *                                  tokenSeparator: 'z',
 *                                  scopeFieldName: 'parentPageUrl',
 *                                  scopeStrategy: 'mainDomain', // apple.com or apple.co.uk
 *                                  expirationPeriod: 86400000 // 24h
 *                              },
 *                              parentPageUrl: {
 *                                  scope: 'hostname' // www.apple.com
 *                              }
 *                          }
 *                      },
 *                      {
 *                          filters: {
 *                              valueMatches: {
 *                                  eventType: ['click'],
 *                                  actionType: ['signUp']
 *                              },
 *                              nonEmptyFields: ['pageHistory']
 *                          },
 *                          fieldConstraints: {
 *                              parentPageUrl: {
 *                                  scope: 'fullWithoutParams'
 *                              }
 *                          }
 *                      },
 *                      {
 *                          filters: {
 *                              valueMatches: {
 *                                  userType: ['signedIn']
 *                              }
 *                          },
 *                          fieldConstraints: {
 *                              clientId: {
 *                                  scopeStrategy: 'all'
 *                              },
 *                              dsId: { blacklisted: true }
 *                          }
 *                      }
 *                  ]
 *              }
 *          }
 *      } }
 *
 *     new Constraints(config).constraintsForEvent({ eventType: 'click', userType: 'signedIn', actionType: 'navigate', ... }) returns:
 *     {
 *         fieldConstraints: {
 *             clientId: {
 *                 generateValue: true, // from 'any' match
 *                 tokenSeparator: 'z', // from 'any' match
 *                 scopeFieldName: 'parentPageUrl', // from 'any' match
 *                 scopeStrategy: 'all', // from userType=signdIn match
 *                 expirationPeriod: 86400000 // from 'any' match
 *             },
 *             dsId: { blacklisted: true }, // from userType=signedIn match
 *             parentPageUrl: {
 *                 scope: 'hostname' // from 'any' match
 *                                   // (the event did not match the rule with eventType=click,
 *                                   // actionType=signUp, nonEmpty pageHistory)
 *             }
 *         }
 *     }
 */
LegacyConstraintGenerator.prototype.constraintsForEvent = function constraintsForEvent(eventData, topicConfig, topic) {
    if (!topicConfig) {
        return Promise.resolve(null);
    }
    var self = this;

    // Use Promise.resolve to wrap the constraintProfiles() here in case of the client delegate the constraintProfile method and returns a non-promise value
    return Promise.resolve(topicConfig.constraintProfile(topic))
        .then(function (constraintProfile) {
            if (!constraintProfile) {
                return null;
            }
            var profilePath = 'constraints.profiles.' + constraintProfile;
            return topicConfig.value(profilePath, topic);
        })
        .then(function (constraintsConfig) {
            var constraints = null;

            if (constraintsConfig && constraintsConfig.precedenceOrderedRules) {
                constraints = constraintsConfig.precedenceOrderedRules.reduce(function (accumulatedRules, rule) {
                    if (self.eventMatchesRule(eventData, rule)) {
                        accumulatedRules = self.updateRules(accumulatedRules, rule);
                    }

                    return accumulatedRules;
                }, {});
            }

            return constraints;
        });
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} matchRule contains information about whether an event matches a rule
 * @return {Boolean}
 * @example
 *     var event = { eventType: 'click', userType: 'signedIn', actionType: 'navigate', ... };
 *     var matchRule = {
 *           filters: {
 *              valueMatches: {
 *                  eventType: ['click']
 *              },
 *              nonEmptyFields: ['actionType']
 *          },
 *          fieldConstraints: { ... }
 *     };
 *     eventMatchesRule(event, matchRule); // => true
 */
LegacyConstraintGenerator.prototype.eventMatchesRule = function eventMatchesRule(eventData, matchRule) {
    var returnValue = false;

    if (eventData && matchRule.filters) {
        if (matchRule.filters === 'any') {
            returnValue = true;
        } else if (reflect.isObject(matchRule.filters)) {
            returnValue =
                this.eventMatchesNonEmptyFields(eventData, matchRule.filters.nonEmptyFields) &&
                this.eventMatchesFieldValues(eventData, matchRule.filters.valueMatches);
        }
    }

    return returnValue;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Array<string>} nonEmptyFieldNames
 * @return {Boolean}
 */
LegacyConstraintGenerator.prototype.eventMatchesNonEmptyFields = function eventMatchesNonEmptyFields(
    eventData,
    nonEmptyFieldNames
) {
    var returnValue = false;

    if (eventData) {
        if (!nonEmptyFieldNames || !reflect.isArray(nonEmptyFieldNames)) {
            returnValue = true;
        } else {
            returnValue = nonEmptyFieldNames.every(function (fieldName) {
                return matchers.nonEmpty(fieldName, eventData);
            });
        }
    }

    return returnValue;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} valueMatches a mapping of field names to lists of possible values to match for that field.  A matching field value is determined using strict equality.  Field values can be of any primitive type.
 * @return {Boolean}
 */
LegacyConstraintGenerator.prototype.eventMatchesFieldValues = function eventMatchesFieldValues(
    eventData,
    valueMatches
) {
    var returnValue = false;

    if (eventData) {
        if (!valueMatches || !reflect.isObject(valueMatches) || reflect.isEmptyObject(valueMatches)) {
            returnValue = true;
        } else {
            returnValue = Object.keys(valueMatches).every(function (fieldMatchName) {
                var fieldMatchValues = valueMatches[fieldMatchName];
                return matchers.valueMatches(fieldMatchName, eventData, fieldMatchValues);
            });
        }
    }

    return returnValue;
};

/**
 * @param {Object} currentRules a dictionary of constraint rules
 * @param {Object} newRule a dictionary of rule data to be added to the current rules
 * @return {Object} the updated set of rules
 * Note: the current rules will be modified in place and also returned
 */
LegacyConstraintGenerator.prototype.updateRules = function updateRules(currentRules, newRule) {
    return updateFieldRulesets(currentRules, newRule, 'fieldConstraints');
};

/*
 *  src/event_actions/denylisted_event_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var DenylistedEventAction = function DenylistedEventAction() {};

/**
 * denylisting the event if the denylisted parameter is true
 * @param {Object} eventData a dictionary of event data
 * @param {Object} originalEventData the original event data
 * @param {Boolean} isDenylisted true if denylisting the entire event
 * @return {Object} return the passed-in eventData if  denylisted not equals to false, otherwise, return "null"
 */
DenylistedEventAction.prototype.performAction = function performAction(eventData, originalEventData, isDenylisted) {
    return isDenylisted !== true ? eventData : null;
};

/*
 *  src/event_actions/denylisted_fields_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2021 Apple Inc. All rights reserved.
 *
 */

var DenylistedFieldsAction = function DenylistedFieldsAction() {};

/**
 * remove the denylisted event fields from the passed-in eventData
 * @param {Object} eventData a dictionary of event data
 * @param {Object} originalEventData the original event data
 * @param {Array} denylistedFields the denylisted fields
 * @return {Object} return a dictionary of event data that excluded the denylisted fields or "null" if all fields are removed.
 */
DenylistedFieldsAction.prototype.performAction = function performAction(
    eventData,
    originalEventData,
    denylistedFields
) {
    if (!eventData || !reflect.isArray(denylistedFields) || reflect.isEmptyArray(denylistedFields)) {
        return eventData;
    }
    eventData = reflect.extend({}, eventData);

    denylistedFields.forEach(function (denylistedField) {
        delete eventData[denylistedField];
    });

    return reflect.isEmptyObject(eventData) ? null : eventData;
};

/*
 *  src/event_actions/allowlisted_fields_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2021 Apple Inc. All rights reserved.
 *
 */

var AllowlistedFieldsAction = function AllowlistedFieldsAction() {};

/**
 * remove the event fields from the passed-in eventData if it is not in the allowlistedFields
 * @param {Object} eventData a dictionary of event data
 * @param {Object} originalEventData the original event data
 * @param {Array} allowlistedFields the allowlisted fields
 * @return {Object} return a dictionary of event data that only included the allowlisted fields
 */
AllowlistedFieldsAction.prototype.performAction = function performAction(
    eventData,
    originalEventData,
    allowlistedFields
) {
    // Ignoring an empty allowlistedFields to have consistent behavior with Native MetricsKit
    if (!eventData || !reflect.isArray(allowlistedFields) || reflect.isEmptyArray(allowlistedFields)) {
        return eventData;
    }
    var returnedData = {};

    allowlistedFields.forEach(function (allowlistedField) {
        if (reflect.isDefinedNonNull(eventData[allowlistedField])) {
            returnedData[allowlistedField] = eventData[allowlistedField];
        }
    });

    return !reflect.isEmptyObject(returnedData) ? returnedData : null;
};

/*
 *  src/event_actions/sessionization_fields_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2023 Apple Inc. All rights reserved.
 *
 */

var MT_SESSIONIZATION_NAMESPACE = 'mtSessionization';
var STORAGE_KEY_SEPARATOR$1 = '_';
var ID_TOKEN_SEPARATOR = '-';
var SESSION_ID_KEY = 'sessionId';
var SESSION_START_TIME_KEY = 'sessionStartTime';
var SessionizationFieldsAction = function SessionizationFieldsAction(constraintsInstance) {
    this._constraintsInstance = constraintsInstance;
};

/**
 * attach the session related fields to the event
 * @param {Object} eventData - a dictionary of event data
 * @param {Object} originalEventData - the original event data
 * @param {Object} sessionRules - the session rule object
 * @param {String}(optional) sessionRules.storageKeyPrefix - a prefix to be used when storing ID data in localStorage, default is "mtSessionization"
 * @param {String}(optional) sessionRules.namespace - a string to be used when storing session metadata in localStorage.
 * @param {String}(optional) sessionRules.scopeFieldName - the field name to indicate the scope for the session metadata
 * @param {Number}(optional) sessionRules.idVersion - the version of the session ID
 * @param {String}(optional) sessionRules.tokenSeparator - the separator used to tokenize sections of a finalized, formatted ID string. Default is 'z'
 * @param {Boolean}(optional) sessionRules.sessionStartTime - the flag to indicate whether record "sessionStartTime". Default is false
 * @param {String}(optional) sessionRules.endSessionConditions - the object that contains the conditions to end the existing session
 * @param {String}(optional) sessionRules.endSessionConditions.lifespan - the maximum lifespan for the session (milliseconds)
 * @param {String}(optional) sessionRules.endSessionConditions.idleSpan - the maximum idle span to end the session (milliseconds)
 * @param {String}(optional) sessionRules.endSessionConditions.eventCount - the maximum event count for the session
 * @param {Object}(optional) sessionRules.sessionResetOptions - the reset session options to determine resetting session (deleting the session metadata from storage)
 * @param {Object}           sessionRules.sessionResetOptions.filters - the filters-like conditions to execute the session reset
 * @param {Boolean}(optional) sessionRules.sessionResetOptions.newSessionAfterReset - the flag to indicate whether generate a new session after resetting the previous session. Default is false
 * @param {Object}(optional) sessionRules.sessionFields - the mapping of the session field in the event payload
 * @return {Object} return a dictionary of event data that included the session fields
 */
SessionizationFieldsAction.prototype.performAction = function performAction(
    eventData,
    originalEventData,
    sessionRules
) {
    if (!reflect.isDefinedNonNull(eventData) || !reflect.isDefined(sessionRules)) {
        return eventData;
    }

    if (reflect.isDefinedNonNull(sessionRules.sessionResetOptions)) {
        if (!reflect.isDefinedNonNull(sessionRules.sessionResetOptions.filters)) {
            throw new SyntaxError('sessionizationFields Action: unable to find the required config "filters"');
        }
        var newSessionAfterReset = this._resetSession(eventData, originalEventData, sessionRules);
        if (newSessionAfterReset !== true) {
            return eventData;
        }
    }

    eventData = reflect.extend({}, eventData);

    var storageKey = this._storageKey(eventData, sessionRules);
    var environment = this._constraintsInstance.system.environment;
    var sessionMetadata = storage.objectFromStorage(environment.localStorageObject(), storageKey) || {};

    if (this._shouldCreateNewSession(originalEventData, sessionMetadata, sessionRules)) {
        sessionMetadata.sessionId = this._generateSessionId(sessionRules);
        sessionMetadata.rawFirstEventTimeInSession = originalEventData.eventTime;
        sessionMetadata.firstEventTimeInSession = eventData.eventTime;
        sessionMetadata.eventCount = 0;
    }
    sessionMetadata.rawLastEventTimeInSession = originalEventData.eventTime;
    sessionMetadata.lastEventTimeInSession = eventData.eventTime;
    sessionMetadata.eventCount += 1;

    storage.saveObjectToStorage(environment.localStorageObject(), storageKey, sessionMetadata);

    var sessionFieldMap = this._getSessionFieldNames(sessionRules);
    eventData[sessionFieldMap.sessionId] = sessionMetadata.sessionId;
    if (sessionRules.sessionStartTime) {
        eventData[sessionFieldMap.sessionStartTime] = sessionMetadata.firstEventTimeInSession;
    }

    return eventData;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} sessionRules includes information about how to namespace/scope the session data
 * @return {String} the key that session data should be stored under
 * @example
 * (storageKeyPrefix ? storageKeyPrefix : mtSessionization)_<namespace>_(scopeFieldName ? <eventData[scopeFieldName]> : '')
 */
SessionizationFieldsAction.prototype._storageKey = function _storageKey(eventData, sessionRules) {
    var scope = this._scope(eventData, sessionRules);
    return this._storageKeyPrefix(sessionRules) + (!reflect.isEmptyString(scope) ? STORAGE_KEY_SEPARATOR$1 + scope : '');
};

/**
 * @param {Object} sessionRules includes information about how to namespace/scope the session data
 * @return {String} a prefix to be used when storing session data in localStorage
 */
SessionizationFieldsAction.prototype._storageKeyPrefix = function _storageKeyPrefix(sessionRules) {
    return sessionRules && reflect.isString(sessionRules.storageKeyPrefix) && sessionRules.storageKeyPrefix.length > 0
        ? sessionRules.storageKeyPrefix
        : MT_SESSIONIZATION_NAMESPACE;
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} sessionRules includes information about how to namespace/scope the session data
 * @return {String} the namespace/scope for this set of event data and rules
 */
SessionizationFieldsAction.prototype._scope = function _scope(eventData, sessionRules) {
    var sessionScope = '';

    if (reflect.isDefined(sessionRules)) {
        if (reflect.isString(sessionRules.namespace)) {
            sessionScope += sessionRules.namespace;
        }

        if (
            reflect.isString(sessionRules.scopeFieldName) &&
            reflect.isDefinedNonNull(eventData[sessionRules.scopeFieldName])
        ) {
            sessionScope += STORAGE_KEY_SEPARATOR$1;
            sessionScope += eventData[sessionRules.scopeFieldName].toString();
        }
    }

    return sessionScope;
};

/**
 * generate session ID based on the provided session rules
 * @param {Object} sessionRules
 * @returns {String} the generated session ID
 * @private
 */
SessionizationFieldsAction.prototype._generateSessionId = function _generateSessionId(sessionRules) {
    return generateId({
        idVersion: 1,
        time: Date.now(),
        idTokenSeparator: ID_TOKEN_SEPARATOR,
        generatedIdSeparator: sessionRules.tokenSeparator
    });
};

/**
 * Decide whether create a new session based on the current session metadata and the session rules
 * @param sessionMetadata
 * @param sessionRules
 * @returns {Boolean}
 * @private
 */
SessionizationFieldsAction.prototype._shouldCreateNewSession = function _shouldCreateNewSession(
    event,
    sessionMetadata,
    sessionRules
) {
    var shouldCreateNewSession = false;
    shouldCreateNewSession |= !reflect.isDefinedNonNull(sessionMetadata.sessionId);

    if (reflect.isDefinedNonNull(sessionRules.endSessionConditions)) {
        if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.lifespan)) {
            shouldCreateNewSession |=
                event.eventTime >=
                sessionMetadata.rawFirstEventTimeInSession + sessionRules.endSessionConditions.lifespan;
        }

        if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.idleSpan)) {
            shouldCreateNewSession |=
                event.eventTime >=
                sessionMetadata.rawLastEventTimeInSession + sessionRules.endSessionConditions.idleSpan;
        }

        if (reflect.isDefinedNonNull(sessionRules.endSessionConditions.eventCount)) {
            shouldCreateNewSession |= sessionMetadata.eventCount >= sessionRules.endSessionConditions.eventCount;
        }
    }

    return shouldCreateNewSession;
};

/**
 * Reset the existing session based on the session rules
 * @param {Object} eventData 
 * @param {Object} originalEventData 
 * @param {Object} sessionRules 
 * @returns {Boolean} The flag indicates whether to process the rest of logic
 */
SessionizationFieldsAction.prototype._resetSession = function _resetSession(
    eventData,
    originalEventData,
    sessionRules
) {
    var sessionResetOptions = sessionRules.sessionResetOptions;
    var constraintsGenerator = this._constraintsInstance._constraintGenerator;
    if (
        reflect.isDefinedNonNull(constraintsGenerator) &&
        reflect.isDefinedNonNull(constraintsGenerator.eventMatchesTreatment) &&
        constraintsGenerator.eventMatchesTreatment(originalEventData, sessionResetOptions)
    ) {
        var storageKey = this._storageKey(eventData, sessionRules);
        var environment = this._constraintsInstance.system.environment;
        storage.saveObjectToStorage(environment.localStorageObject(), storageKey, undefined);

        return sessionResetOptions.newSessionAfterReset;
    }
    // Always continue the sessionization logic if the resetting is not applied.
    return true;
};

/**
 * Return a map from session field names to their associated event field names
 * @param {Object} sessionRoles 
 * @returns {Object} A map between the session field names and their associated event field names
 */
SessionizationFieldsAction.prototype._getSessionFieldNames = function _getSessionFieldNames(sessionRoles) {
    var sessionFieldNames = {
        sessionId: SESSION_ID_KEY,
        sessionStartTime: SESSION_START_TIME_KEY
    };
    if (reflect.isDefinedNonNull(sessionRoles.sessionFields)) {
        if (reflect.isDefinedNonNullNonEmpty(sessionRoles.sessionFields[SESSION_ID_KEY])) {
            sessionFieldNames.sessionId = sessionRoles.sessionFields[SESSION_ID_KEY];
        }
        if (reflect.isDefinedNonNullNonEmpty(sessionRoles.sessionFields[SESSION_START_TIME_KEY])) {
            sessionFieldNames.sessionStartTime = sessionRoles.sessionFields[SESSION_START_TIME_KEY];
        }
    }

    return sessionFieldNames;
};

/*
 *  src/event_actions/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var ACTIONS = {
    blacklistedEventAction: 'blacklisted', //  DEPRECATED, use denylistedEventAction instead
    denylistedEventAction: 'denylisted',
    blacklistedFieldsAction: 'blacklistedFields', //  DEPRECATED, use denylistedFieldsAction instead
    denylistedFieldsAction: 'denylistedFields',
    whitelistedFieldsAction: 'whitelistedFields', // DEPRECATED, use allowlistedFieldsAction instead
    allowlistedFieldsAction: 'allowlistedFields',
    sessionizationFieldsAction: 'sessionizationFields'
};

var EventActions = function EventActions(constraintsInstance) {
    var denylistedEventAction = new DenylistedEventAction();
    var denylistedFieldsAction = new DenylistedFieldsAction();
    var allowlistedFieldsAction = new AllowlistedFieldsAction();
    var sessionizationFieldsAction = new SessionizationFieldsAction(constraintsInstance);

    // @private
    this._actions = {};
    this._actions[ACTIONS.blacklistedEventAction] = denylistedEventAction; // mapping to equivalent but Inclusive Termed method
    this._actions[ACTIONS.denylistedEventAction] = denylistedEventAction;
    this._actions[ACTIONS.blacklistedFieldsAction] = denylistedFieldsAction; // mapping to equivalent but Inclusive Termed method
    this._actions[ACTIONS.denylistedFieldsAction] = denylistedFieldsAction;
    this._actions[ACTIONS.whitelistedFieldsAction] = allowlistedFieldsAction; // mapping to equivalent but Inclusive Termed method
    this._actions[ACTIONS.allowlistedFieldsAction] = allowlistedFieldsAction;
    this._actions[ACTIONS.sessionizationFieldsAction] = sessionizationFieldsAction;
};

EventActions.prototype.getAction = function getAction(actionName) {
    return this._actions[actionName];
};

/*
 *  src/field_actions/number_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var START_KEY = 'start';
var VALUE_KEY = 'value';

/**
 * Returns the index at which you should insert the object in order to maintain a sorted array
 * @param {Array} array - The sorted array to inspect, must be a list without undefined/null values.
 * @param {Number} value - The value to evaluate
 * @returns {number} Returns the index at which `value` should be inserted, -1 if the value is less than the first item, the array or value is undefined/null
 */
var searchInsertionIndexOf = function searchInsertionIndexOf(array, value) {
    var NOT_FOUND_OUTPUT = -1;
    var index = NOT_FOUND_OUTPUT;

    if (
        !reflect.isDefinedNonNull(value) ||
        array.length === 0 ||
        // classify the numbers less than the lowest bucket
        // -> array = [10, 20, 30], value = 9
        // <- -1
        (reflect.isDefinedNonNull(array[0]) && value < array[0][START_KEY])
    ) {
        return NOT_FOUND_OUTPUT;
    }

    // Using a linear search instead of binary search because the array won't be large and less error-prone
    if (array[array.length - 1][START_KEY] < value) {
        index = array.length - 1;
    } else {
        for (var i = 0; i < array.length; i++) {
            var start = array[i][START_KEY];
            if (start === value) {
                index = i;
                break;
            } else if (start > value) {
                index = i - 1;
                break;
            }
        }
    }

    return index;
};

var NumberAction = function NumberAction() {
    Base$1.apply(this, arguments);
};

NumberAction.prototype = Object.create(Base$1.prototype);
NumberAction.prototype.constructor = NumberAction;

/**
 * @param {Number} aNumber - a number being constrained
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @param {Number} fieldRules.precision - must be a positive integer
 * @return {Number} the constrained number
 */
NumberAction.prototype.constrainedValue = function constrainedValue(aNumber, fieldRules) {
    var precision = fieldRules ? fieldRules.precision : 0;
    var buckets = fieldRules ? fieldRules.buckets : null;

    if (reflect.isDefinedNonNullNonEmpty(buckets)) {
        buckets = buckets.slice().sort(function (a, b) {
            return a[START_KEY] - b[START_KEY];
        });

        var bucketIndex = searchInsertionIndexOf(buckets, aNumber);
        var bucket = buckets[bucketIndex];

        if (reflect.isDefinedNonNull(bucket)) {
            aNumber = bucket[VALUE_KEY];
        }
    } else if (reflect.isNumber(aNumber) && reflect.isNumber(precision) && precision > 0) {
        aNumber = Math.floor(aNumber / precision) * precision;
    }

    return aNumber;
};

/*
 *  src/utils/serial_number_generator.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var DEFAULT_NAMESPACE = 'mt_serial_number';
var EXPIRATION_STORAGE_KEY = 'exp';
var SERIAL_NUMBER_STORAGE_KEY = 'sn';

/**
 *
 * @param {Object} options - An object that contains parameters for generating serial numbers
 * @param {String} options.namespace (optional) - A key that is used to store the serial numbers in the Storage. Default to "mt_serial_number"
 * @param {Number} options.initialSerialNumber (optional) - An initialization serial number. Default to 0
 * @param {Number} options.nextRotationTime (optional) - A timestamp that indicates when should reset the serial number. Default to Number.POSITIVE_INFINITY(never rotate)
 * @param {Number} options.rotationPeriod (optional) - A millisecond to indicate how long the serial number could be alive. Default to Number.POSITIVE_INFINITY(never rotate)
 * @constructor
 */
var SerialNumberGenerator = function SerialNumberGenerator(options) {
    options = options || {};
    // @private
    this._nextRotationTime = options.nextRotationTime || Number.POSITIVE_INFINITY;
    // @private
    this._storageKey = options.namespace || DEFAULT_NAMESPACE;
    // @private
    this._initialSerialNumber = options.initialSerialNumber || 0;
    // @private
    this._rotationPeriod = options.rotationPeriod || Number.POSITIVE_INFINITY;
};

SerialNumberGenerator.prototype.setDelegate = function setDelegate(delegate) {
    reflect.attachDelegate(this, delegate);
};

SerialNumberGenerator.prototype.localStorageObject = function localStorageObject() {
    return storage.localStorageObject();
};

/**
 * Return the increased serial number
 * @param {Number} increment (optional) - The amount to increment. Defaults to 1
 * @returns {Number} the increased serial number
 */
SerialNumberGenerator.prototype.getNextSerialNumber = function getNextSerialNumber(increment) {
    var storageKey = this._storageKey;
    var serialNumberData = this._getCurrentSerialNumberData(storageKey);

    var serialNum = serialNumberData[SERIAL_NUMBER_STORAGE_KEY];
    increment = reflect.isNumber(increment) ? increment : 1;
    serialNum = parseInt(serialNum, 10);

    if (isNaN(serialNum)) {
        // Reset the serial number to the initialized one, to ensure the logic won't break if the sequence number is an invalid number.
        serialNum = this._initialSerialNumber;
    }
    serialNum = this._increaseSerialNumber(serialNum, increment);
    // Store the increased serial number to storage
    serialNumberData[SERIAL_NUMBER_STORAGE_KEY] = serialNum;
    storage.saveObjectToStorage(this.localStorageObject(), this._storageKey, serialNumberData);

    return serialNum;
};

/**
 * Reset the serial number
 */
SerialNumberGenerator.prototype.resetSerialNumber = function resetSerialNumber() {
    var serialNumberData = storage.objectFromStorage(this.localStorageObject(), this._storageKey);

    if (reflect.isDefinedNonNull(serialNumberData)) {
        this._resetSerialNumber(serialNumberData[EXPIRATION_STORAGE_KEY]);
    }
};

/**
 * Delegable method to return the time for calculating rotation
 * @returns {number}
 */
SerialNumberGenerator.prototype.getTime = function getTime() {
    return Date.now();
};

/**
 * Increasing the giving serial number by plus one
 * @param {Number} serialNum
 * @returns {number}
 * @private
 */
SerialNumberGenerator.prototype._increaseSerialNumber = function _increaseSerialNumber(serialNum, increment) {
    return serialNum + increment;
};

/**
 * Rotate and return the serial number data
 * @param {String} storageKey
 * @returns {Object} rotated serial number data
 */
SerialNumberGenerator.prototype._getCurrentSerialNumberData = function _getCurrentSerialNumberData(storageKey) {
    var serialNumberData = storage.objectFromStorage(this.localStorageObject(), storageKey);
    var rotationTime;
    var nextRotationTime;
    if (serialNumberData) {
        rotationTime = serialNumberData[EXPIRATION_STORAGE_KEY];
        rotationTime = parseInt(rotationTime, 10);
        serialNumberData[EXPIRATION_STORAGE_KEY] = rotationTime = isNaN(rotationTime)
            ? this._nextRotationTime
            : rotationTime;
    } else {
        // use the "nextRotationTime - rotationPeriod" as the rotationTime if the serialNumberData is not existing in the storage, to check if need to reset serial number
        rotationTime = this._nextRotationTime - this._rotationPeriod;
    }

    // Reset the serial number data if it has expired or never initialized
    // Checking "!serialNumberData" in here to cover the case of when both of this._nextRotationTime and this._rotationPeriod are not provided, "this._nextRotationTime(Infinite) - this._rotationPeriod(Infinite) = NaN" which is always less than "this.getTime()"
    // Use while loop here to catch up to the latest rotation time.
    while (!serialNumberData || this.getTime() >= rotationTime) {
        rotationTime = nextRotationTime = rotationTime + this._rotationPeriod;
        serialNumberData = this._resetSerialNumber(nextRotationTime);
    }
    return serialNumberData;
};

/**
 * Reset the serial number and expiration
 * @param {Number} nextRotationTime - A timestamp that indicates when should reset the serial number
 * @returns reset serialNumberData
 * {
 *     exp: nextRotationTime,
 *     sn: serialNumber
 * }
 */
SerialNumberGenerator.prototype._resetSerialNumber = function _resetSerialNumber(nextRotationTime) {
    var serialNumberData = {};
    serialNumberData[EXPIRATION_STORAGE_KEY] = nextRotationTime;
    serialNumberData[SERIAL_NUMBER_STORAGE_KEY] = this._initialSerialNumber;
    storage.saveObjectToStorage(this.localStorageObject(), this._storageKey, serialNumberData);
    return serialNumberData;
};

/*
 *  src/field_actions/time_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var STORAGE_KEY_SEPARATOR$2 = '_';
var STORAGE_PREFIX_DEFAULT = 'mtTimestamp';

var TimeAction = function TimeAction() {
    Base$1.apply(this, arguments);
    // @private
    this._storage = this._constraintsInstance.system.environment.localStorageObject();
    // @private
    /*
     * Store the end time of the giving time precision base on namespace + time fields
     */
    this._precisionEndTimeCache = {};
    // @private
    this._serialNumberGenerator = null;
};

TimeAction.prototype = Object.create(Base$1.prototype);
TimeAction.prototype.constructor = TimeAction;

/**
 * @param {Number} time - a timestamp being constrained
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @param {Object} fieldRules.precision - The time must be a positive integer
 * @param {String} fieldRules.storageKeyPrefix - a prefix to be used when storing timestamp de-res related data, default is "mt-timestamp"
 * @param {String} fieldRules.namespace - a namespace for the timestamp de-res related data, default is empty.
 * @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
 * @param {String} fieldName - name of the field being modified in eventData
 * @return {Number} the constrained time or the original value if time is not defined or fieldRules is unavailable
 */
TimeAction.prototype.constrainedValue = function constrainedValue(time, fieldRules, eventData, fieldName) {
    var returnTimestamp = time;
    if (
        reflect.isNumber(time) &&
        reflect.isObject(fieldRules) &&
        reflect.isNumber(fieldRules.precision) &&
        fieldRules.precision > 0
    ) {
        var precisionStartTime = this._computePrecisionStartTime(time, fieldRules);
        this._serialNumberGenerator = new SerialNumberGenerator({
            namespace: this._persistentStorageKey(fieldRules, fieldName),
            nextRotationTime: precisionStartTime + fieldRules.precision,
            rotationPeriod: fieldRules.precision
        });
        this._serialNumberGenerator.setDelegate(this._constraintsInstance.system.environment);
        this._serialNumberGenerator.setDelegate({
            getTime: function () {
                return time;
            }
        });
        var serialNumber = this._serialNumberGenerator.getNextSerialNumber();
        returnTimestamp = this._computeTimestamp(precisionStartTime, serialNumber);
        this._serialNumberGenerator = null; // Release the serial number generator.
    }

    return returnTimestamp;
};

TimeAction.prototype._computeTimestamp = function _computeTimestamp(precisionStartTime, sequenceNum) {
    return precisionStartTime + sequenceNum;
};

TimeAction.prototype._persistentStorageKey = function _persistentStorageKey(fieldRules, fieldName) {
    var namespaceSegment = fieldRules.namespace ? STORAGE_KEY_SEPARATOR$2 + fieldRules.namespace : '';
    return (
        (fieldRules.storageKeyPrefix || STORAGE_PREFIX_DEFAULT) + namespaceSegment + STORAGE_KEY_SEPARATOR$2 + fieldName
    );
};

TimeAction.prototype._computePrecisionStartTime = function _computePrecisionStartTime(time, fieldRules) {
    var precision = fieldRules.precision;
    return Math.floor(time / precision) * precision;
};

/*
 *  src/field_actions/hash_action.js
 *  mt-client-constraints
 *
 *  Copyright © 2022 Apple Inc. All rights reserved.
 *
 */

var STORAGE_KEY_SEPARATOR$3 = '_';
var STORAGE_PREFIX_DEFAULT$1 = 'mtHash';
var STORAGE_SALT_KEY = 'salt';
var SALT_CHAR_LENGTH = 10;

var HashAction = function HashAction() {
    Base$1.apply(this, arguments);
};
HashAction.prototype = Object.create(Base$1.prototype);
HashAction.prototype.constructor = HashAction;

/**
 * Build the storage key for salt data
 * key format: <prefix|mtHash>_<namespace?>_salt_<fieldName>
 * @param fieldRules
 * @param fieldName
 * @returns {string}
 */
function buildSaltStorageKey(fieldRules, fieldName) {
    var namespaceSegment = fieldRules.namespace ? STORAGE_KEY_SEPARATOR$3 + fieldRules.namespace : '';
    return (
        (fieldRules.storageKeyPrefix || STORAGE_PREFIX_DEFAULT$1) +
        namespaceSegment +
        STORAGE_KEY_SEPARATOR$3 +
        STORAGE_SALT_KEY +
        STORAGE_KEY_SEPARATOR$3 +
        fieldName
    );
}

function generateSalt() {
    var salt = '';
    while (salt.length < SALT_CHAR_LENGTH) {
        salt += string.randomHexCharacter();
    }
    return salt;
}

// The hash logic is borrowed from String.hashcode() of Java
function hashCode(value, salt) {
    return [value, salt]
        .map(function (segment) {
            var hash = 0;
            // undefined, null and '' will return 0 as the hash code.
            if (reflect.isDefinedNonNullNonEmpty(segment)) {
                for (var i = 0; i < segment.length; i++) {
                    var charCode = segment.charCodeAt(i);
                    hash = (hash << 5) - hash + charCode; // "(hash << 5) - hash" is similar to "hash * 31" but faster.
                }
            }
            var hashedValue = Math.abs(hash);
            hashedValue = parseInt(hashedValue, 16);
            return string.convertNumberToBaseAlphabet(hashedValue, string.base62Alphabet);
        })
        .join('');
}

/**
 *
 * @param {String} value The value that will be hashed
 * @param {Object} fieldRules - includes information about how to constrain the field
 * @param {String} fieldRules.storageKeyPrefix - a prefix to be used when storing hash related data, default is "mtHash"
 * @param {String} fieldRules.namespace - a namespace for storing the hash data, default is empty.
 * @param {Number} fieldRules.saltLifespan - a lifespan (milliseconds) of the salt
 * @param {Object} fieldRules.platformBasedSalt - a config section includes the configures for loading salt from platform API
 * @param {String} fieldRules.platformBasedSalt.saltNamespace - a namespace that stores the salt configuration in the "metricsIdentifier" section of the bag
 * @param {String} fieldRules.platformBasedSalt.crossDeviceSync
 * @param {Object} eventData - a dictionary of event data, which should include a pre-existing (unconstrained) field
 * @param {String} fieldName - name of the field being modified in eventData
 * @return {String | Promise<string>} The hashed value on the top of provided value with the stored salt (rotated for every <fieldRules.saltLifespan> milliseconds).
 */
HashAction.prototype.constrainedValue = function constrainedValue(value, fieldRules, _eventData, fieldName) {
    if (reflect.isDefinedNonNullNonEmpty(value)) {
        return this._loadPlatformBasedSalt(fieldRules, fieldName).then(function (salt) {
            return hashCode(value, salt);
        });
    }
    return value;
};

/**
 * @param {Number} timestamp a timestamp in ms since epoch
 * @return {Boolean} return false if timestamp does not exist
 * @overridable
 */
HashAction.prototype.timeExpired = function timeExpired(timestamp) {
    return timestamp ? timestamp <= Date.now() : false;
};

/**
 * @param {Number} (optional) lifespan the amount of time, in milliseconds, that an ID should be valid for
 * @return {Number} a timestamp in ms since epoch, or null if no lifespan was provided
 * @overridable
 */
HashAction.prototype.expirationTime = function expirationTime(lifespan) {
    return lifespan ? Date.now() + lifespan : null;
};

HashAction.prototype._loadPlatformBasedSalt = function _loadPlatformBasedSalt(fieldRules, fieldName) {
    var saltPromise = null;
    var self = this;
    var platformBasedSaltConfig = fieldRules.platformBasedSalt;
    if (reflect.isDefinedNonNull(platformBasedSaltConfig)) {
        saltPromise = this._constraintsInstance.system.environment.platformIdentifier(
            platformBasedSaltConfig.saltNamespace,
            'userid',
            platformBasedSaltConfig.crossDeviceSync || true
        );
        if (reflect.isDefinedNonNull(saltPromise)) {
            saltPromise = saltPromise.then(function (salt) {
                if (!reflect.isDefinedNonNull(salt)) {
                    self._constraintsInstance.system.logger.warn(
                        'Hash: platform returned an empty salt. Will use default salt generator to generate the salt.'
                    );
                    salt = self._getSalt(fieldRules, fieldName);
                }
                return salt;
            });
        } else {
            saltPromise = Promise.resolve(this._getSalt(fieldRules, fieldName));
        }
    } else {
        saltPromise = Promise.resolve(this._getSalt(fieldRules, fieldName));
    }

    return saltPromise;
};

// This method retrieves the salt from storage, otherwise generates a new salt if it doesn't exist or has expired
HashAction.prototype._getSalt = function _getSalt(fieldRules, fieldName) {
    var saltMetadata = this._retrieveSaltFromStorage(fieldRules, fieldName);
    var saltLifespan = fieldRules.saltLifespan;
    if (!reflect.isDefinedNonNull(saltMetadata) || this.timeExpired(saltMetadata.expirationTime)) {
        var localStorage = this._constraintsInstance.system.environment.localStorageObject();
        var salt = generateSalt();
        saltMetadata = {
            salt: salt,
            expirationTime: this.expirationTime(saltLifespan)
        };
        storage.saveObjectToStorage(localStorage, buildSaltStorageKey(fieldRules, fieldName), saltMetadata);
    }

    return saltMetadata.salt;
};

HashAction.prototype._retrieveSaltFromStorage = function _retrieveSaltFromStorage(fieldRules, fieldName) {
    var localStorage = this._constraintsInstance.system.environment.localStorageObject();
    var saltMetadata = storage.objectFromStorage(localStorage, buildSaltStorageKey(fieldRules, fieldName));
    return saltMetadata;
};

/*
 *  src/field_actions/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var ACTIONS$1 = {
    ID: 'idGenerator',
    NUMBER: 'numberDeres',
    TIME: 'timeDeres',
    URL: 'urlDeres',
    HASH: 'hash'
};

var FieldActions = function DeresHandlers(constraintsInstance) {
    this.actions = {};
    this.actions[ACTIONS$1.ID] = new IdAction(constraintsInstance);
    this.actions[ACTIONS$1.NUMBER] = new NumberAction(constraintsInstance);
    this.actions[ACTIONS$1.TIME] = new TimeAction(constraintsInstance);
    this.actions[ACTIONS$1.URL] = new UrlAction(constraintsInstance);
    this.actions[ACTIONS$1.HASH] = new HashAction(constraintsInstance);
};

FieldActions.prototype.getAction = function getAction(actionName) {
    return this.actions[actionName];
};

/*
 *  src/treatment/action_treatment.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

var ActionTreatment = function ActionTreatment(constraintInstance) {
    // @private
    this._eventActions = new EventActions(constraintInstance);

    // @private
    this._fieldActions = new FieldActions(constraintInstance);
};

/**
 * @param {Object} eventData a dictionary of event data
 * @param {Object} eventConstraints a set of constraints to apply to this event
 * @return {Object | Promise} constraints the event data modified according to the appropriate constraints or "null" if the event is blacklisted
 * Note:
 *  1. create a new dictionary if the event data is constrained/modified
 *  2. return the original eventData if the constraints parameter is null or an empty dictionary
 * @example
 * var eventData = {
 *     eventType: 'click',
 *     pageType: 'TopCharts',
 *     parentPageUrl: 'https://itunes.apple.com/music/topcharts/12345',
 *     // etc.
 * };
 * var eventConstraints = {
 *         eventActions: { blacklistedFields: ['cookies', 'pageDetails'] },
 *         fieldActions: {
 *              parentPageUrl: {
 *                  treatmentType: 'urlDeres',
 *                  scope: 'hostname'
 *              }
 *         }
 *     }
 * constraints.eventFields.applyEventConstraints(eventData, eventConstraints) =>
 *     {
 *         eventType: click,
 *         pageType: 'TopCharts',
 *         parentPageUrl: 'itunes.apple.com', // truncated to hostname only
 *         etc... // all other fields remain the same, except "cookies", "pageDetails"
 *     }
 */
ActionTreatment.prototype.applyConstraints = function applyConstraints(eventData, constraints) {
    var returnEventData = eventData; // Set the original eventData to the returning variable to return the original eventData if neither event actions nor field actions were applied.

    if (constraints && !reflect.isEmptyObject(constraints)) {
        var promiseTasks = [];
        var self = this;

        if (constraints.fieldActions && !reflect.isEmptyObject(constraints.fieldActions)) {
            var isAnyFieldChanged = false;
            var eventDataCopy = returnEventData;
            eventDataCopy = Object.keys(constraints.fieldActions).reduce(function (accumulatedFields, fieldName) {
                var fieldRules = constraints.fieldActions[fieldName];
                if (fieldRules) {
                    var denylisted = fieldRules.denylisted || fieldRules.blacklisted;
                    var fieldAction = fieldRules.treatmentType;
                    var fieldActionHandler = self._fieldActions.getAction(fieldAction);

                    accumulatedFields = lookForKeyPath(
                        accumulatedFields,
                        fieldName,
                        false,
                        function (value, key, keyPath, object) {
                            if (denylisted) {
                                delete object[key];
                                isAnyFieldChanged = true;
                            } else if (fieldRules.hasOwnProperty(FIELD_RULES.OVERRIDE_FIELD_VALUE)) {
                                object[key] = fieldRules[FIELD_RULES.OVERRIDE_FIELD_VALUE];
                                isAnyFieldChanged = true;
                            } else if (fieldActionHandler) {
                                var returnedValue = fieldActionHandler.performAction(
                                    value,
                                    fieldName,
                                    returnEventData,
                                    fieldRules
                                );
                                object[key] = returnedValue;
                                if (returnedValue instanceof Promise) {
                                    promiseTasks.push(
                                        returnedValue.then(function (processedValue) {
                                            lookForKeyPath(
                                                eventDataCopy,
                                                fieldName,
                                                true,
                                                function (_value, targetKey, targetKeyPath, targetObject) {
                                                    if (targetKeyPath === keyPath) {
                                                        targetObject[targetKey] = processedValue;
                                                    }
                                                }
                                            );
                                        })
                                    );
                                }
                                isAnyFieldChanged = true;
                            }
                        }
                    );
                }
                return accumulatedFields;
            }, eventDataCopy);

            // If any field has been constrained, we create a new object to contain the merged fields instead of merging the changes to the original eventData.
            // eventDataCopy has been re-built by "lookForKeyPath" above.
            if (isAnyFieldChanged) {
                if (promiseTasks.length > 0) {
                    returnEventData = Promise.all(promiseTasks).then(function () {
                        return eventDataCopy;
                    });
                } else {
                    returnEventData = eventDataCopy;
                }
            }
        }

        // perform event actions after the field actions to ensure removing denied fields or keeping allowed fields for those generated fields(e.g. IdGenerator).
        if (constraints.eventActions && !reflect.isEmptyObject(constraints.eventActions)) {
            var eventActionNames = Object.keys(constraints.eventActions);
            var processEventActions = function (processingEventData) {
                eventActionNames.forEach(function (eventAction) {
                    var eventActionHandler = self._eventActions.getAction(eventAction);
                    if (eventActionHandler) {
                        var actionRules = constraints.eventActions[eventAction];
                        processingEventData = eventActionHandler.performAction(
                            processingEventData,
                            eventData,
                            actionRules
                        );
                    }
                });
                return processingEventData;
            };

            if (returnEventData instanceof Promise) {
                returnEventData = Promise.resolve(returnEventData).then(function (processedEventData) {
                    return processEventActions(processedEventData);
                });
            } else {
                returnEventData = processEventActions(returnEventData);
            }
        }
    }

    return returnEventData;
};

/*
 *  src/constraint_generator/treatment_generator.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

var TREATMENT_FILTERS_FIELD = 'filters';
var TREATMENT_FILTERS_ALL = 'any';
var TREATMENT_EVENT_ACTIONS = 'eventActions';
var TREATMENT_FIELD_ACTIONS = 'fieldActions';

function _updateTreatment(accumulatedTreatment, treatment) {
    var currentTreatment = accumulatedTreatment || {};
    // update event actions
    _updateEventActions(currentTreatment, treatment);
    // update field actions
    _updateFieldActions(currentTreatment, treatment);

    return currentTreatment;
}

function _updateEventActions(targetTreatment, sourceTreatment) {
    if (!targetTreatment[TREATMENT_EVENT_ACTIONS]) {
        targetTreatment[TREATMENT_EVENT_ACTIONS] = {};
    }
    var currentTreatmentEventActions = targetTreatment[TREATMENT_EVENT_ACTIONS];
    var treatmentEventActions = sourceTreatment[TREATMENT_EVENT_ACTIONS];

    if (treatmentEventActions) {
        Object.keys(treatmentEventActions).reduce(function (accumulatedEventActions, eventAction) {
            var existingActionValue = accumulatedEventActions[eventAction];
            var actionValue = treatmentEventActions[eventAction];
            // Merge the event action values from different treatments
            if (reflect.isArray(existingActionValue)) {
                // If the action value is not an array, treat it as a bad data and discard it.
                if (reflect.isArray(actionValue)) {
                    actionValue.forEach(function (value) {
                        if (existingActionValue.indexOf(value) === -1) {
                            existingActionValue.push(value);
                        }
                    });
                }
            } else {
                // Currently only have array type and primitive type parameters. Ignore the other types of parameter values.
                if (reflect.isArray(actionValue)) {
                    // Clone the array value, to avoid the original array is changed by other treatments.
                    accumulatedEventActions[eventAction] = actionValue.slice();
                } else if (
                    reflect.isObject(actionValue) ||
                    (!reflect.isObject(actionValue) && !reflect.isFunction(actionValue))
                ) {
                    // object, primitive type, null and undefined
                    // set the existing action value with the primitive type value.
                    accumulatedEventActions[eventAction] = actionValue;
                }
            }

            return accumulatedEventActions;
        }, currentTreatmentEventActions);
    }
}

function _updateFieldActions(targetTreatment, sourceTreatment) {
    if (!targetTreatment[TREATMENT_FIELD_ACTIONS]) {
        targetTreatment[TREATMENT_FIELD_ACTIONS] = {};
    }
    updateFieldRulesets(
        targetTreatment,
        sourceTreatment,
        TREATMENT_FIELD_ACTIONS,
        function (targetRules, sourceRules, fieldName) {
            // if the target field rule has the same treatmentType as the source field rule, then return the target field rule to replace its rule props with the source ones.
            // otherwise, all of the source field rules will overwrite all of the target field rules
            if (
                targetRules[fieldName] &&
                targetRules[fieldName].treatmentType === sourceRules[fieldName].treatmentType
            ) {
                return targetRules[fieldName];
            } else {
                // if the treatmentType is not the same between field rules, return an empty object to take the latter field rules
                /*
                {
                    treatments: [{
                        ...,
                        fieldActions: {
                            afield: { treatmentType: 'a', propA: 123 }
                        }
                    }, {
                        ...,
                        fieldActions: {
                            afield: { treatmentType: 'b', propB: 123 }
                        }
                    }]
                }

                expected output:
                {
                    treatments: [{
                        ...,
                        fieldActions: {
                            afield: { treatmentType: 'b', propB: 123 }
                        }
                    }]
                }
             */
                return {};
            }
        }
    );
}

var TreatmentGenerator = function TreatmentGenerator(constraintsInstance) {
    // @private
    this._constraintsInstance = constraintsInstance;
    this.treatment = new ActionTreatment(constraintsInstance);
};

/**
 * Combine treatments from multiple profiles
 * @param {Array} ConstraintProfiles the constraint profile names
 * @param {Object} topicConfig An AMP Metrics Config
 * @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
 * @returns {Promise} a Promise that returns an Array of combined treatments from multiple constraint profiles
 * @private
 */
TreatmentGenerator.prototype._combineTreatments = function _combineTreatments(constraintProfiles, topicConfig, topic) {
    var combinedTreatmentsPromise;
    var buildTreatmentTasks = [];

    if (reflect.isArray(constraintProfiles)) {
        constraintProfiles.forEach(function (constraintProfile) {
            if (!constraintProfile) {
                return;
            }

            var profileName = 'treatmentProfiles.' + constraintProfile;
            var constraintsPromise = topicConfig.value(profileName, topic).then(function (constraints) {
                return constraints && constraints.treatments ? constraints.treatments : [];
            });
            buildTreatmentTasks.push(constraintsPromise);
        });

        combinedTreatmentsPromise = Promise.all(buildTreatmentTasks).then(function (profilesTreatments) {
            var combinedTreatments = [];
            profilesTreatments.forEach(function (profileTreatments) {
                combinedTreatments = combinedTreatments.concat(profileTreatments);
            });

            return combinedTreatments;
        });
    } else {
        combinedTreatmentsPromise = Promise.resolve([]);
    }

    return combinedTreatmentsPromise;
};

/**
 * Build the properly constraints for the passed-in event data
 * @param {Object} eventData a dictionary of event data
 * @param {Object} topicConfig An AMP Metrics Config
 * @param {String}(optional) topic defines the AMP Analytics "topic" to look up the constraint profile
 * @return {Object} a set of constraints to apply to this event.
 *                  returns null (MetricsKit will send the original event) if:
 *                      1. no topic config available
 *                      2. defaultTreatmentProfile is undefined
 *                      3. the profile is found but no treatment matched
 * @throws {TypeError} throws a type error if the topic config contains any invalid element.
 * @throws {SyntaxError} throws a syntax error if:
 *                          1. topicConfig.constraintProfiles(topic) is not found in the topic config
 *                          2. the treatment configuration is invalid
 * @overridable
 * Constraint rules will be applied in the order they are provided in config.
 * @example
 *     Given the following config:
 *     metrics: {
 *         ...
 *         low_res_topic: {
 *            defaultTreatmentProfiles: ['iosStores', 'embeddedWeb'],
 *         },
 *         defaultTreatmentProfiles: ['embeddedWeb']
 *         treatmentProfiles: {
 *            iosStores: { version: 1, treatments: [ ... ] },
 *            embeddedWeb: {
 *              version: 2,
 *              treatments: [
 *                  {
 *                      filters: {
 *                          eventType: { valueMatches: ['enter', 'exit' ] },
 *                          isSignedIn: { valueMatches: [true] }
 *                      },
 *                      eventActions: {
 *                          blacklistedFields: ['cookies']
 *                      }
 *                  },
 *                  {
 *                      filters: {
 *                          eventType: { valueMatches: [ 'exit' ] }
 *                      },
 *                      eventActions: {
 *                          blacklistedFields: ['cookies', 'pageDetails']
 *                      },
 *                      fieldActions: {
 *                          clientId: {
 *                              treatmentType: 'idDeres',
 *                              storageKeyPrefix: 'mtClientId',
 *                              namespace: 'test',
 *                              scopeStrategy: 'mainDomain',
 *                              scopeFieldName: 'https://www.apple.com/products/',
 *                              tokenSeparator: 'z',
 *                              lifespan: 86400000
 *                          }
 *                      }
 *                  },
 *                  {
 *                      filters: {
 *                          userType: { valueMatches: ['signedIn'] },
 *                          actionType: { valueMatches: ['navigate'] }
 *                      },
 *                      fieldActions: {
 *                          os: { blacklisted: true },
 *                          // round down time to 1 day
 *                          eventTime: {
 *                              treatmentType: "numberDeres",
 *                              precision: 86400000
 *                          },
 *                          // remove query params
 *                          pageUrl: {
 *                              treatmentType: "urlDeres",
 *                              scope: 'fullWithoutParams'
 *                          },
 *                          // Deres disk available space round down to MB
 *                          capacityDiskAvailable: {
 *                              treatmentType: "numberDeres",
 *                              precision: 1000000, // 1MB
 *                          },
 *                          clientId: {
 *                              treatmentType: 'idDeres',
 *                              scopeFieldName: 'https://www.apple.com/',
 *                              lifespan: 3600000
 *                          }
 *                      }
 *                  }
 *              ]
 *           }
 *        },
 *        ...
 *    }
 *
 *     constraintsForEvent({ eventType: 'exit', isSignedIn: true, userType: 'signedIn', actionType: 'navigate', ... }, topicConfig) returns:
 *     {
 *         eventActions: {
 *              blacklistedFields: ['cookies', 'pageDetails'] // from "eventType: { valueMatches: [ 'exit' ] }", override the one of "treatments[0]"
 *         },
 *         fieldActions: {
 *              os: { blacklisted: true },
 *              eventTime: {
 *                  treatmentType: "numberDeres",
 *                  precision: 86400000
 *              },
 *              pageUrl: {
 *                  treatmentType: "urlDeres",
 *                  scope: 'fullWithoutParams'
 *              },
 *              capacityDiskAvailable: {
 *                  treatmentType: "numberDeres",
 *                  precision: 1000000, // 1MB
 *              },
 *              clientId: {
 *                  treatmentType: 'idDeres',
 *                  storageKeyPrefix: 'mtClientId',
 *                  namespace: 'test',
 *                  scopeStrategy: 'mainDomain',
 *                  scopeFieldName: 'https://www.apple.com/', // override the value from "treatments[1].clientId"
 *                  tokenSeparator: 'z',
 *                  lifespan: 3600000 // override the value from "treatments[1].clientId"
 *              }
 *         }
 *     }
 */
TreatmentGenerator.prototype.constraintsForEvent = function constraintsForEvent(eventData, topicConfig, topic) {
    if (!topicConfig) {
        return Promise.resolve(null);
    }
    var self = this;

    // Use Promise.resolve to wrap the constraintProfiles() here in case of the client delegate the constraintProfiles method and returns non-promise value
    return Promise.resolve(topicConfig.constraintProfiles(topic))
        .then(function (constraintProfiles) {
            // Adapt the v1 profile to v2 profiles
            if (!reflect.isDefinedNonNull(constraintProfiles)) {
                return Promise.resolve(topicConfig.constraintProfile(topic)).then(function (constraintProfile) {
                    return reflect.isDefinedNonNull(constraintProfile) ? [constraintProfile] : null;
                });
            } else {
                return constraintProfiles;
            }
        })
        .then(function (constraintProfiles) {
            // rdar://71993234 if there is no default treatment profile and the client did not declare a treatment profile, do not modify the event
            if (reflect.isDefinedNonNull(constraintProfiles)) {
                if (!reflect.isArray(constraintProfiles)) {
                    throw new TypeError(
                        '"constraintProfiles" should be an Array, but got: ' +
                            (constraintProfiles ? constraintProfiles.constructor : constraintProfiles)
                    );
                }
                return self
                    ._combineTreatments(constraintProfiles, topicConfig, topic)
                    .then(function (combinedTreatments) {
                        // rdar://71993234 if the treatments are not found in the topic config
                        if (combinedTreatments.length === 0) {
                            throw new SyntaxError(
                                'The constraintProfiles: ' +
                                    constraintProfiles.join(', ') +
                                    ' are not found in the topic config'
                            );
                        }
                        return combinedTreatments;
                    });
            } else {
                return Promise.resolve([]);
            }
        })
        .then(function (combinedTreatments) {
            var returnTreatments = combinedTreatments.reduce(function (accumulatedTreatment, treatment) {
                if (self.eventMatchesTreatment(eventData, treatment)) {
                    accumulatedTreatment = _updateTreatment(accumulatedTreatment, treatment);
                }
                return accumulatedTreatment;
            }, null);

            return returnTreatments;
        });
};

TreatmentGenerator.prototype.eventMatchesTreatment = function eventMatchesTreatment(eventData, treatment) {
    var filters = treatment[TREATMENT_FILTERS_FIELD];

    // Fast false for free-form filter since JS does not support it at the moment
    // Applying the treatment to all events if there is no filters to align the behavior with the native implementation.
    if (!reflect.isDefinedNonNull(filters)) {
        return true;
    }
    // Applying the treatment to all events if the value equals to "any"
    if (reflect.isString(filters)) {
        return filters === TREATMENT_FILTERS_ALL;
    }

    // If the filter element is an empty filter list. We consider it is an incorrect config.
    if (Object.keys(filters).length === 0) {
        throw new SyntaxError('Unable to find the filter in \n' + JSON.stringify(treatment));
    }

    return Object.keys(filters).every(function (filterField) {
        var fieldFilters = filters[filterField];

        // Fast false for free-form filter since JS does not support it at the moment
        if (fieldFilters && reflect.isString(fieldFilters)) {
            return false;
        }
        // if a field isn't an object or doesn't have any matchers. We consider this is a bad filter and discard the event
        if (!fieldFilters || !reflect.isObject(fieldFilters) || reflect.isEmptyObject(fieldFilters)) {
            throw new SyntaxError(
                'Invalid filter object for field (' + filterField + ') in \n' + JSON.stringify(treatment)
            );
        }
        // Only return the treatments where all treatments match.
        // Current, only one condition for one field.
        return Object.keys(fieldFilters).every(function (matcherName) {
            var matcherParam = fieldFilters[matcherName];

            if (matchers[matcherName]) {
                return matchers[matcherName](filterField, eventData, matcherParam);
            } else {
                throw new SyntaxError(
                    'Unable to find the filter (' +
                        matcherName +
                        ') for field (' +
                        filterField +
                        ')in \n' +
                        JSON.stringify(treatment)
                );
            }
        });
    });
};

/*
 *  src/config.js
 *  mt-client-constraints
 *
 *  Copyright © 2020 Apple Inc. All rights reserved.
 *
 */

/**
 * The constraints config delegate
 * Constraints attach this delegate to the topic config to have constraints features on the topic config
 */
var constraintsConfig = {
    /**
     * Return the constraint profile from a config with constraint syntax v1
     * @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
     * @return {Promise} a Promise that returns the name of the constraint profile from constraint syntax v1 to use
     */
    constraintProfile: function constraintProfile(topic) {
        return this.value('constraints.defaultProfile', topic);
    },

    /**
     * Return the constraint profiles from a config with constraint syntax v2
     * @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
     * @return {Promise} a Promise that returns an array of the names of the constraint profile to use
     */
    constraintProfiles: function constraintProfiles(topic) {
        return this.value('defaultTreatmentProfiles', topic);
    }
};

/*
 *  src/index.js
 *  mt-client-constraints
 *
 *  Copyright © 2017-2018 Apple Inc. All rights reserved.
 *
 */

function _validateConfig(config) {
    var isValid = true;

    isValid &= reflect.isDefinedNonNull(config);
    if (isValid) {
        isValid &= !reflect.isEmptyObject(config);
        isValid &= reflect.isFunction(config.initialized);
        isValid &= reflect.isFunction(config.value);
        isValid &= reflect.isFunction(config.constraintProfile);
    }

    return isValid;
}

/**
 * Attaching config related methods for Constraints
 * @param {Config} topicConfig An AMP Metrics Config
 * @returns {Config} the passed-in config with constraint-related methods attached
 */
function connectConstraintConfig(topicConfig) {
    // return the topic config if it has already been attached with the Constraint methods.
    if (reflect.isFunction(topicConfig.constraintProfile) && reflect.isFunction(topicConfig.constraintProfiles)) {
        return topicConfig;
    }
    reflect.attachMethods(topicConfig, constraintsConfig, topicConfig);

    return topicConfig;
}

/**
 * Supplies the single JavaScript entrypoint to constraint functionality
 * Since JavaScript is prototype-based and not class-based, and doesn't provide
 * an "official" object model, this API is presented as a functional API, but
 * still retains the ability to override and customize functionality via the
 * "setDelegate()" method. In this way, it doesn't carry with it the spare
 * baggage of exposing a bolt-on object model which may differ from a bolt-on
 * (or homegrown) object model already existing in the app.
 * @module
 * @param {Object} topicConfig a topic config
 * @param {Object} delegates
 * @constructor
 *
 * @example
 * import * as delegates from '@amp-metrics/mt-metricskit-delegates-html';
 * import Constraints, { connectConstraintConfig } from '@amp-metrics/mt-client-constraints';
 * import Config from '@amp-metrics/mt-client-config';
 *
 * const topicConfig = new Config('topic');
 * connectConstraintConfig(topicConfig);
 *
 * var eventData = {...};
 * var constraints = new Constraints(topicConfig, delegates);
 * var constrainedEventData = constraints.applyConstraintTreatments(eventData);
 */
var Constraints = function Constraints(topicConfig, delegate) {
    if (!_validateConfig(topicConfig)) {
        throw new Error('The topic config is not a valid instance of "mt-client-config".');
    }

    // @private
    this._isInitialized = false;

    // @private
    this._topicConfig = topicConfig;

    /**
     * constraint generator for specific topic config
     * @type {ConstraintGenerator}
     */
    // @private
    this._constraintGenerator = null;

    /**
     * system/platform-specific classes
     */
    this.system = new System();

    reflect.setDelegates(this.system, delegate || {});
};

/**
 * get constraint generator based on the Constraints' config
 * @returns {Promise} a Promise that returns the active constraint generator
 */
Constraints.prototype._getConstraintGenerator = function _getConstraintGenerator() {
    var self = this;

    if (this._constraintGenerator) {
        return Promise.resolve(this._constraintGenerator);
    } else {
        return this._topicConfig.value('treatmentProfiles').then(function (treatmentConfig) {
            if (reflect.isDefinedNonNull(treatmentConfig)) {
                self._constraintGenerator = new TreatmentGenerator(self);
            } else {
                self._constraintGenerator = new LegacyConstraintGenerator(self);
            }
            return self._constraintGenerator;
        });
    }
};

/**
 * Build constraints with the eventData
 * @param {Object} eventData a dictionary of event data
 * @param {String}(optional) topic defines the AMP Analytics "topic" that this event should be stored under
 * @return {Promise} a Promise that returns a set of constraints to apply to this event
 * @throws {SyntaxError/TypeError} throws a type error if the topic config contains any invalid element.
 */
Constraints.prototype.constraintsForEvent = function constraintsForEvent(eventData, topic) {
    var self = this;
    return this._getConstraintGenerator().then(function (constraintGenerator) {
        return constraintGenerator.constraintsForEvent(eventData, self._topicConfig, topic);
    });
};

/**
 * Apply the given eventData with associated constraints
 * @param {Object} eventData a dictionary of event data
 * @param {Object}(optional) constraints a set of constraints to apply to this event
 * @returns {Promise} a Promise that returns the performed event Data with the given constraints or null if the event is blacklisted or should be discard
 */
Constraints.prototype.applyConstraintTreatments = function applyConstraints(eventData, constraints) {
    var constraintsPromise = constraints ? Promise.resolve(constraints) : this.constraintsForEvent(eventData);
    var self = this;

    return Promise.all([constraintsPromise, this._getConstraintGenerator()])
        .then(function (output) {
            var constraints = output[0];
            var constraintGenerator = output[1];
            return constraintGenerator.treatment.applyConstraints(eventData, constraints);
        })
        .catch(function (e) {
            self.system.logger.warn('An error occurred while applying constraints: ' + e.message || e);
            return null;
        });
};

export default Constraints;
export { connectConstraintConfig };
