"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isNullDecision = exports.getAudiencesConditionsTypes = exports.experimentTargetsByLocation = exports.selectExperimentFromLayer = exports.decideExperiments = void 0;
const optimizely_lite_es_1 = __importDefault(require("@optimizely/optimizely-sdk/dist/optimizely.lite.es"));
const uuid_1 = require("../utils/uuid");
const utils_1 = __importStar(require("../utils"));
const generate_data_1 = require("../datafile/generate_data");
const error_1 = require("./error");
const bucketing_1 = require("./bucketing");
const campaign_fns_1 = require("./campaign_fns");
const condition_1 = __importDefault(require("../lib/condition"));
const browser_1 = __importDefault(require("../lib/matchers/browser"));
const cookie_1 = __importDefault(require("../lib/matchers/cookie"));
const device_1 = __importDefault(require("../lib/matchers/device"));
const first_session_1 = __importDefault(require("../lib/matchers/first_session"));
const ip_1 = __importDefault(require("../lib/matchers/ip"));
const language_1 = __importDefault(require("../lib/matchers/language"));
const location_1 = __importDefault(require("../lib/matchers/location"));
const query_1 = __importDefault(require("../lib/matchers/query"));
const platform_1 = __importDefault(require("../lib/matchers/platform"));
const campaign_1 = __importDefault(require("../lib/matchers/campaign"));
const referrer_1 = __importDefault(require("../lib/matchers/referrer"));
const device_source_1 = __importDefault(require("../lib/device_source"));
const source_type_1 = __importDefault(require("../lib/source_type"));
const time_and_day_1 = __importDefault(require("../lib/matchers/time_and_day"));
const source_type_2 = __importDefault(require("../lib/matchers/source_type"));
const location_2 = __importDefault(require("../lib/location"));
const logger_1 = require("../logger");
const campaign_2 = __importDefault(require("../lib/matchers/campaign"));
const constants_1 = require("../constants");
const index_1 = require("../lib/matchers/index");
const attributes_1 = require("../attributes");
const optlyInstancesMap = new Map();
/**
 * TODO:  move this to ../lib/matchers/index.js ad get the matchers evaliuators from there
 * Returns a map of audience condition evaluators by condition type.
 
 * @return {Object.<{evaluate: *}>}
 */
function getConditionEvaluators() {
    return {
        browser_version: {
            evaluate: (cond, attributes) => browser_1.default.match(attributes, cond),
        },
        campaign: {
            evaluate: (cond, attributes) => campaign_1.default.match(attributes, cond),
        },
        cookies: {
            evaluate: (cond, attributes) => cookie_1.default.match(attributes, cond),
        },
        device: {
            evaluate: (cond, attributes) => device_1.default.match(attributes, cond),
        },
        first_session: {
            evaluate: (cond, attributes) => first_session_1.default.match(attributes),
        },
        ip: {
            evaluate: (cond, attributes) => ip_1.default.match(attributes.visitorIp, cond),
        },
        language: {
            evaluate: (cond, attributes) => language_1.default.match(attributes, cond),
        },
        location: {
            evaluate: (cond, attributes) => location_1.default.match(attributes, cond),
        },
        platform: {
            evaluate: (cond, attributes) => platform_1.default.match(attributes, cond),
        },
        query: {
            evaluate: (cond, attributes) => query_1.default.match(attributes.queryParams, cond),
        },
        referrer: {
            evaluate: (cond, attributes) => referrer_1.default.match(attributes, cond),
        },
        source_type: {
            evaluate: (cond, attributes) => source_type_2.default.match(attributes, cond),
        },
        time_and_day: {
            evaluate: (cond, attributes) => time_and_day_1.default.match(attributes, cond),
        },
    };
}
function omitUndefinedValues(obj) {
    return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined));
}
/**
 * Return Optimizely instance for the given manifest revision.
 *
 * @param {Object} manifest
 */
function getOptimizelyInstance(manifest) {
    const revisionSymbol = manifest.revision;
    if (optlyInstancesMap.has(revisionSymbol)) {
        const optlyInstance = optlyInstancesMap.get(revisionSymbol);
        optlyInstance.decisionService.forcedVariationMap = {};
        (0, logger_1.getGlobalLogHandler)().info('Optly instance from weakmap.');
        return optlyInstance;
    }
    const datafile = (0, generate_data_1.convertWebManifestToDatafile)(manifest);
    let fsOptions = {
        datafile: datafile,
        UNSTABLE_conditionEvaluators: getConditionEvaluators(),
        skipJSONValidation: true,
        logLevel: 'debug',
        logger: logger_1.globalLogHandler,
    };
    const optlyInstance = optimizely_lite_es_1.default.createInstance(fsOptions);
    optlyInstancesMap.set(revisionSymbol, optlyInstance);
    (0, logger_1.getGlobalLogHandler)().info('Optly instance created.');
    return optlyInstance;
}
/**
 * Make a decision for each experiment based on the active viewIds.
 * @param {Array<String>} activeViewIds
 * @param {string} visitorId
 * @param {Object} webManifest
 * @param {Object} inputs
 *        Note: May be mutated, enriched with data like the visitor's location.
 * @return {Promise.<Array.<Decision>>}
 */
async function decideExperiments(activeViewIds, visitorId, webManifest, visitorProfile, inputs, variationMap, logger) {
    const optlyInstance = getOptimizelyInstance(webManifest);
    const { forceVariationIds = [] } = inputs;
    let layersWithForcedVariations;
    if (forceVariationIds.length) {
        if (webManifest.enableForceParameters) {
            layersWithForcedVariations = setForceVariations(optlyInstance, webManifest, visitorId, forceVariationIds, logger);
        }
        else {
            logger.logLifeCycleEvent(`Not forcing variations ${forceVariationIds} because force parameters is disabled.`);
        }
    }
    const forceVariationsAreInEffect = (forceVariationIds || []).length > 0 &&
        webManifest.enableForceParameters;
    // Checking if an ID is in this set is done a lot, so make it O(1) time.
    activeViewIds = new Set(activeViewIds);
    const decisionOptions = {
        activeViewIds,
        checkHoldback: !forceVariationsAreInEffect,
        visitorProfile,
        inputs,
        optlyInstance,
        visitorId,
        variationMap,
        webManifest,
    };
    const edgeAudienceIds = webManifest.audiences
        .filter(audience => condition_1.default.allAudienceConditionsAreEdge(audience.conditions))
        .map(audience => audience.id);
    const decisionBatches = webManifest.layers
        .filter(layer => isABTestOrCampaign(layer, logger) &&
        refersToActiveView(activeViewIds, layer, logger) &&
        refersToEdgeAudiencesOnly(edgeAudienceIds, layer, logger) &&
        (!forceVariationsAreInEffect ||
            layersWithForcedVariations.includes(layer.id)))
        .map(layer => decideLayer(decisionOptions, layer, edgeAudienceIds, logger));
    const decisions = (await Promise.all(decisionBatches)).flat().map(d => {
        if (!isNullDecision(d)) {
            // null decisions were already logged at time of creation in decideLayer
            logger.logDecision(d);
        }
        return d;
    });
    return decisions;
}
exports.decideExperiments = decideExperiments;
/**
 * @param {Boolean} checkHoldback
 * @param {Object} options
 * @param {Boolean} options.checkHoldback
 *        Whether to evaluate the visitor for being in the holdback.
 * @param {String} options.visitorId
 * @param {Object} options.optlyInstance
 * @param {Set.<String>} options.activeViewIds
 * @param {Object} options.inputs
 *        To be mutated just-in-time, enriched with lazy visitor profile attributes
 *        like location.
 * @param {Object} options.webManifest
 * @param {Object} layer
 *        A well-formed A/B Test layer.
 * @returns {Promise.<Array.<Decision>|null>}
 *        One element for each active view that the given layer refers to.
 *        Always fulfills.
 *
 */
async function decideLayer({ checkHoldback, visitorId, optlyInstance, activeViewIds, visitorProfile, inputs, variationMap, webManifest, }, layer, edgeAudienceIds, logger) {
    let decisionSansViewId;
    let experiment;
    if (checkHoldback) {
        const isInLayerHoldback = (0, bucketing_1.isHoldback)(visitorId, layer);
        if (isInLayerHoldback) {
            logger.logLifeCycleEvent(`Visitor is in holdback for layer ${layer.id}`);
            decisionSansViewId = {
                layerId: layer.id,
                isLayerHoldback: true,
                // Not sure this is the right behavior.  in the snippet it still selecting
                // the variation and experiment even for this cases.
                experimentId: null,
                variationId: null,
            };
            return generateDecisionPerView(layer, activeViewIds, decisionSansViewId);
        }
    }
    if (layer.policy === constants_1.ExperimentType.SINGLE_EXPERIMENT ||
        layer.policy === constants_1.ExperimentType.MULTIVARIATE) {
        // ab test logic
        experiment = layer.experiments[0];
        return getLocationAndVariation({
            visitorProfile,
            inputs,
            experiment,
            webManifest,
            optlyInstance,
            visitorId,
            layer,
            activeViewIds,
            stickyVariation: variationMap[layer.id],
            logger,
        });
    }
    else if (layer.policy === constants_1.ExperimentType.EQUAL_PRIORITY) {
        // personalization campaign logic
        return await selectExperimentFromLayer({
            visitorProfile,
            inputs,
            experiment,
            webManifest,
            optlyInstance,
            visitorId,
            layer,
            activeViewIds,
            stickyVariation: variationMap[layer.id],
            logger,
            edgeAudienceIds,
        });
    }
    else {
        throw new error_1.DecisionError(`Invalid layer policy "${layer.policy}" in layer ${layer.id}`);
    }
}
async function getLocationAndVariation({ visitorProfile, inputs, experiment, webManifest, optlyInstance, visitorId, layer, activeViewIds, stickyVariation, logger, }) {
    const attributesNeeded = (0, index_1.getMatcherFieldsNeeds)(getAudiencesConditionsTypes(webManifest, experiment.audienceIds));
    await (0, attributes_1.enrichProfile)(attributesNeeded, visitorProfile, inputs);
    // TODO: use experiment.audienceIds, webManifest.audiences to get the "conditions" for the needed matchers
    // then use the attributes to enrich the visitor profile
    let decisionSansViewId = {};
    let variationId = optlyInstance.getVariation(experiment.id, visitorId, omitUndefinedValues(visitorProfile.profile));
    // if variationId have any value, it means that the user is in the experiment.
    // but because the sticky variation is set
    const selectedStickyVariation = stickyVariation && stickyVariation[experiment.id];
    let stickyDecision = false;
    if (selectedStickyVariation && variationId) {
        stickyDecision = true;
        if (selectedStickyVariation === variationId) {
            logger.logLifeCycleEvent(`Visitor is in the sticky variation ${variationId} for experiment ${experiment.id}`);
        }
        else {
            logger.logLifeCycleEvent(`Visitor is in the variation ${variationId} for experiment ${experiment.id} but the sticky variation is ${selectedStickyVariation}`);
            variationId = selectedStickyVariation;
        }
    }
    if (!variationId) {
        logger.logLifeCycleEvent(`No variation chosen for experiment ${experiment.id}`);
        decisionSansViewId = {
            layerId: layer.id,
            isLayerHoldback: false,
            experimentId: null,
            variationId: null,
        };
        return generateDecisionPerView(layer, activeViewIds, decisionSansViewId);
    }
    const variation = experiment.variations.find(v => v.id === variationId);
    if (!variation) {
        logger.debug('Decision / Preferred variation not found on experiment:', experiment.id, '. Visitor not bucketed.');
    }
    decisionSansViewId = {
        layerId: layer.id,
        experimentId: experiment.id,
        experimentName: experiment.name,
        variationId: variation?.id,
        variationName: variation?.name,
        stickyDecision: stickyDecision,
    };
    // Return a Decision for each active view that this layer refers to
    return generateDecisionPerView(layer, activeViewIds, decisionSansViewId);
}
async function selectExperimentFromLayer({ visitorProfile, inputs, experiment, webManifest, optlyInstance, visitorId, layer, activeViewIds, logger, edgeAudienceIds, }) {
    if (!layer.decisionMetadata ||
        !layer.decisionMetadata.experimentPriorities) {
        throw new error_1.DecisionError('No decisionMetadata.experimentPriorities on layer.');
    }
    var priorityLevels = layer.decisionMetadata.experimentPriorities.length;
    for (var i = 0; i < priorityLevels; i++) {
        let selectedExperiment = await (0, campaign_fns_1.selectExperiment)(layer, edgeAudienceIds, visitorId, i, logger);
        if (selectedExperiment && !(0, utils_1.isEmpty)(selectedExperiment)) {
            let decisions = await getLocationAndVariation({
                visitorProfile,
                inputs,
                experiment: selectedExperiment,
                webManifest,
                optlyInstance,
                visitorId,
                layer,
                activeViewIds,
                logger,
            });
            let decision = decisions.find(each => each && each.variationId) || {};
            let variationId = decision.variationId;
            let selectedVariation;
            if (variationId) {
                selectedVariation = selectedExperiment.variations.find(variation => variation.id === variationId);
            }
            else {
                continue;
            }
            if (!layer.decisionMetadata.offerConsistency ||
                (0, campaign_fns_1.hasVariationActionsOnView)(selectedVariation, decision.viewId)) {
                return decision;
            }
        }
    }
    logger.error('No eligible experiment found for layer: ', layer.id);
    let decisionSansViewId = {
        layerId: layer.id,
        isLayerHoldback: false,
        experimentId: null,
        variationId: null,
    };
    return generateDecisionPerView(layer, activeViewIds, decisionSansViewId);
}
exports.selectExperimentFromLayer = selectExperimentFromLayer;
/**
 * Generates decisions for each active view that the layer refers to.
 * Returns these decisions in an array.
 * @param {Object} layer
 *        A well-formed A/B Test layer.
 * @param {Set.<String>} activeViewIds
 * @param {Object} decisionSansViewId
 * @returns {<Array.<Decision>}
 */
function generateDecisionPerView(layer, activeViewIds, decisionSansViewId) {
    return layer.viewIds
        .filter(viewId => activeViewIds.has(viewId))
        .map(viewId => {
        let timestamp = Date.now();
        let decisionId = (0, uuid_1.generate_uuid)();
        return Object.assign({}, decisionSansViewId, { viewId }, { decisionId }, { timestamp });
    });
}
/**
 * @param {Object} layer
 * @returns {Boolean}
 *        Whether the layer is a well-formed A/B Test layer OR an ordered (personalization campaign) test layer.
 */
function isABTestOrCampaign(layer, logger) {
    if (layer.policy === constants_1.ExperimentType.SINGLE_EXPERIMENT ||
        layer.policy === constants_1.ExperimentType.EQUAL_PRIORITY ||
        layer.policy === constants_1.ExperimentType.MULTIVARIATE) {
        return layer.experiments && layer.experiments.length;
    }
    logger.logError(new error_1.DecisionError(`Invalid layer policy "${layer.policy}" in layer ${layer.id}`));
    return false;
}
/**
 * @param {Set.<String>} activeViewIds
 * @param {Object} layer
 * @returns {Boolean}
 *        Whether the given layer refers to any active views.
 */
function refersToActiveView(activeViewIds, layer, logger) {
    if (layer.viewIds.some(id => activeViewIds.has(id))) {
        return true;
    }
    logger.logLifeCycleEvent(`Not deciding layer ${layer.id} because it doesn't contain any active views`);
    return false;
}
/**
 * @param {Set.<String>} edgeAudienceIds
 * @param {Object} layer
 * @returns {Boolean}
 *        Whether the given layer refers only to Audience Conditions which are evaluatable at the Edge.
 */
function refersToEdgeAudiencesOnly(edgeAudienceIds, layer, logger) {
    let edgeAudiencesOnly = true;
    layer.experiments.forEach(experiment => {
        if ((experiment.audienceIds || []).length > 0 &&
            !experiment.audienceIds.every(id => ['and', 'or'].includes(id) ||
                (edgeAudienceIds || []).includes(id))) {
            logger.logLifeCycleEvent(`Not deciding layer ${layer.id} because at least one audience cannot be evaluated at the Edge`);
            edgeAudiencesOnly = false;
        }
    });
    return edgeAudiencesOnly;
}
/**
 * Setup force variations for this instance of the SDK for the force variationIds provided.
 * @param {Optimizely} optlyInstance - The instance of Optimizely.
 * @param {Object} webManifest - A Web manifest that was translated to the datafile that the Optimizely instance
 *                               was instantiated with.
 * @param {String} visitorId - The visitorId to use for force bucketing.
 * @param {Array} forceVariationIds - The list of variation IDs to force.
 * @return {Array} A list of layer IDs that contain the forced variations.
 *
 * Note: variation and experiment keys are the string values of the respective entity's id.
 *       As such, we can safely pass force variation ids (as strings) to this method.
 *       See src/datafile/generate_data.js for where they are generated.
 */
function setForceVariations(optlyInstance, webManifest, visitorId, forceVariationIds, logger) {
    const variationToExperimentMap = {};
    const variationToLayerMap = {};
    const layersWithForcedVariations = [];
    webManifest.layers.forEach(layer => {
        layer.experiments &&
            layer.experiments.forEach(experiment => {
                experiment.variations.forEach(variation => {
                    variationToExperimentMap[variation.id] = experiment.id;
                    variationToLayerMap[variation.id] = layer.id;
                });
            });
    });
    forceVariationIds.forEach(variationId => {
        if (!variationToExperimentMap[variationId]) {
            let newError = new error_1.DecisionError(`Variation not found for force variationId=${variationId}`);
            logger.logError(newError);
            return;
        }
        logger.logLifeCycleEvent(`Forcing user into variation ${variationId}`);
        optlyInstance.setForcedVariation(variationToExperimentMap[variationId], visitorId, variationId);
        layersWithForcedVariations.push(variationToLayerMap[variationId]);
    });
    return layersWithForcedVariations;
}
/**
 * @param {Object} webManifest
 * @param {Object} experiment
 * @returns {Boolean}
 *        Whether some audience in the given experiment's targeting conditions
 *        contains a condition of type 'location'.
 * @throws {DecisionError}
 *        If there's an inconsistency in the WebManifest such that some audience to
 *        which a layer refers is missing from the WebManifest.
 */
function experimentTargetsByLocation(webManifest, experiment) {
    function getConditionsForAudience(audienceId) {
        const audience = webManifest.audiences.find(({ id }) => id === audienceId);
        if (!audience) {
            throw new error_1.DecisionError(`Experiment "${experiment.id}" references nonexistent audience "${audienceId}"`);
        }
        return audience.conditions;
    }
    // A null audienceIds => experiment is targeted at Everyone; no constraints.
    const audienceTargetingTree = (experiment.audienceIds || []).filter(id => !['and', 'or'].includes(id));
    return audienceTargetingTree.some(audienceId => {
        return getConditionsForAudience(audienceId)
            .flat(Infinity)
            .some(cond => cond.type === 'location');
    });
}
exports.experimentTargetsByLocation = experimentTargetsByLocation;
function getAudiencesConditionsTypes(webManifest, audienceIds) {
    if (!audienceIds) {
        return [];
    }
    const audiencesConditions = webManifest.audiences
        .filter(({ id }) => audienceIds.includes(id))
        .map(audience => audience.conditions);
    return Array.from(new Set(audiencesConditions.flat(Infinity).map(cond => cond.type)));
}
exports.getAudiencesConditionsTypes = getAudiencesConditionsTypes;
/**
 * A null decision is one where the visitor is not bucketed into a variation because they are in
 * the holdback or do not meet the audience conditions for the experiment. This function
 * returns whether or not the decision is a null one.
 *
 * @param {Object} decision
 * @returns {Boolean}
 */
function isNullDecision(decision) {
    return !decision.variationId;
}
exports.isNullDecision = isNullDecision;
