import { evalCondition } from "./mongrule.mjs";
import { chooseVariation, decrypt, getBucketRanges, getQueryStringOverride, getUrlRegExp, hash, inNamespace, inRange, isIncluded, isURLTargeted, toString } from "./util.mjs";
export const EVENT_FEATURE_EVALUATED = "Feature Evaluated";
export const EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
function getForcedFeatureValues(ctx) {
  // Merge user and global values
  const ret = new Map();
  if (ctx.global.forcedFeatureValues) {
    ctx.global.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
  }
  if (ctx.user.forcedFeatureValues) {
    ctx.user.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
  }
  return ret;
}
function getForcedVariations(ctx) {
  // Merge user and global values
  if (ctx.global.forcedVariations && ctx.user.forcedVariations) {
    return {
      ...ctx.global.forcedVariations,
      ...ctx.user.forcedVariations
    };
  } else if (ctx.global.forcedVariations) {
    return ctx.global.forcedVariations;
  } else if (ctx.user.forcedVariations) {
    return ctx.user.forcedVariations;
  } else {
    return {};
  }
}
async function safeCall(fn) {
  try {
    await fn();
  } catch (e) {
    // Do nothing
  }
}
function onExperimentViewed(ctx, experiment, result) {
  // Make sure a tracking callback is only fired once per unique experiment
  if (ctx.user.trackedExperiments) {
    const k = getExperimentDedupeKey(experiment, result);
    if (ctx.user.trackedExperiments.has(k)) {
      return [];
    }
    ctx.user.trackedExperiments.add(k);
  }
  if (ctx.user.enableDevMode && ctx.user.devLogs) {
    ctx.user.devLogs.push({
      experiment,
      result,
      timestamp: Date.now().toString(),
      logType: "experiment"
    });
  }
  const calls = [];
  if (ctx.global.trackingCallback) {
    const cb = ctx.global.trackingCallback;
    calls.push(safeCall(() => cb(experiment, result, ctx.user)));
  }
  if (ctx.user.trackingCallback) {
    const cb = ctx.user.trackingCallback;
    calls.push(safeCall(() => cb(experiment, result)));
  }
  if (ctx.global.eventLogger) {
    const cb = ctx.global.eventLogger;
    calls.push(safeCall(() => cb(EVENT_EXPERIMENT_VIEWED, {
      experimentId: experiment.key,
      variationId: result.key,
      hashAttribute: result.hashAttribute,
      hashValue: result.hashValue
    }, ctx.user)));
  }
  return calls;
}
function onFeatureUsage(ctx, key, ret) {
  // Only track a feature once, unless the assigned value changed
  if (ctx.user.trackedFeatureUsage) {
    const stringifiedValue = JSON.stringify(ret.value);
    if (ctx.user.trackedFeatureUsage[key] === stringifiedValue) return;
    ctx.user.trackedFeatureUsage[key] = stringifiedValue;
    if (ctx.user.enableDevMode && ctx.user.devLogs) {
      ctx.user.devLogs.push({
        featureKey: key,
        result: ret,
        timestamp: Date.now().toString(),
        logType: "feature"
      });
    }
  }
  if (ctx.global.onFeatureUsage) {
    const cb = ctx.global.onFeatureUsage;
    safeCall(() => cb(key, ret, ctx.user));
  }
  if (ctx.user.onFeatureUsage) {
    const cb = ctx.user.onFeatureUsage;
    safeCall(() => cb(key, ret));
  }
  if (ctx.global.eventLogger) {
    const cb = ctx.global.eventLogger;
    safeCall(() => cb(EVENT_FEATURE_EVALUATED, {
      feature: key,
      source: ret.source,
      value: ret.value,
      ruleId: ret.source === "defaultValue" ? "$default" : ret.ruleId || "",
      variationId: ret.experimentResult ? ret.experimentResult.key : ""
    }, ctx.user));
  }
}
export function evalFeature(id, ctx) {
  if (ctx.stack.evaluatedFeatures.has(id)) {
    process.env.NODE_ENV !== "production" && ctx.global.log(`evalFeature: circular dependency detected: ${ctx.stack.id} -> ${id}`, {
      from: ctx.stack.id,
      to: id
    });
    return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
  }
  ctx.stack.evaluatedFeatures.add(id);
  ctx.stack.id = id;

  // Global override
  const forcedValues = getForcedFeatureValues(ctx);
  if (forcedValues.has(id)) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Global override", {
      id,
      value: forcedValues.get(id)
    });
    return getFeatureResult(ctx, id, forcedValues.get(id), "override");
  }

  // Unknown feature id
  if (!ctx.global.features || !ctx.global.features[id]) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Unknown feature", {
      id
    });
    return getFeatureResult(ctx, id, null, "unknownFeature");
  }

  // Get the feature
  const feature = ctx.global.features[id];

  // Loop through the rules
  if (feature.rules) {
    const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
    rules: for (const rule of feature.rules) {
      // If there are prerequisite flag(s), evaluate them
      if (rule.parentConditions) {
        for (const parentCondition of rule.parentConditions) {
          ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
          const parentResult = evalFeature(parentCondition.id, ctx);
          // break out for cyclic prerequisites
          if (parentResult.source === "cyclicPrerequisite") {
            return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
          }
          const evalObj = {
            value: parentResult.value
          };
          const evaled = evalCondition(evalObj, parentCondition.condition || {});
          if (!evaled) {
            // blocking prerequisite eval failed: feature evaluation fails
            if (parentCondition.gate) {
              process.env.NODE_ENV !== "production" && ctx.global.log("Feature blocked by prerequisite", {
                id,
                rule
              });
              return getFeatureResult(ctx, id, null, "prerequisite");
            }
            // non-blocking prerequisite eval failed: break out of parentConditions loop, jump to the next rule
            process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because prerequisite evaluation fails", {
              id,
              rule
            });
            continue rules;
          }
        }
      }

      // If there are filters for who is included (e.g. namespaces)
      if (rule.filters && isFilteredOut(rule.filters, ctx)) {
        process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of filters", {
          id,
          rule
        });
        continue;
      }

      // Feature value is being forced
      if ("force" in rule) {
        // If it's a conditional rule, skip if the condition doesn't pass
        if (rule.condition && !conditionPasses(rule.condition, ctx)) {
          process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of condition ff", {
            id,
            rule
          });
          continue;
        }

        // If this is a percentage rollout, skip if not included
        if (!isIncludedInRollout(ctx, rule.seed || id, rule.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !rule.disableStickyBucketing ? rule.fallbackAttribute : undefined, rule.range, rule.coverage, rule.hashVersion)) {
          process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because user not included in rollout", {
            id,
            rule
          });
          continue;
        }
        process.env.NODE_ENV !== "production" && ctx.global.log("Force value from rule", {
          id,
          rule
        });

        // If this was a remotely evaluated experiment, fire the tracking callbacks
        if (rule.tracks) {
          rule.tracks.forEach(t => {
            const calls = onExperimentViewed(ctx, t.experiment, t.result);
            if (!calls.length && ctx.global.saveDeferredTrack) {
              ctx.global.saveDeferredTrack({
                experiment: t.experiment,
                result: t.result
              });
            }
          });
        }
        return getFeatureResult(ctx, id, rule.force, "force", rule.id);
      }
      if (!rule.variations) {
        process.env.NODE_ENV !== "production" && ctx.global.log("Skip invalid rule", {
          id,
          rule
        });
        continue;
      }

      // For experiment rules, run an experiment
      const exp = {
        variations: rule.variations,
        key: rule.key || id
      };
      if ("coverage" in rule) exp.coverage = rule.coverage;
      if (rule.weights) exp.weights = rule.weights;
      if (rule.hashAttribute) exp.hashAttribute = rule.hashAttribute;
      if (rule.fallbackAttribute) exp.fallbackAttribute = rule.fallbackAttribute;
      if (rule.disableStickyBucketing) exp.disableStickyBucketing = rule.disableStickyBucketing;
      if (rule.bucketVersion !== undefined) exp.bucketVersion = rule.bucketVersion;
      if (rule.minBucketVersion !== undefined) exp.minBucketVersion = rule.minBucketVersion;
      if (rule.namespace) exp.namespace = rule.namespace;
      if (rule.meta) exp.meta = rule.meta;
      if (rule.ranges) exp.ranges = rule.ranges;
      if (rule.name) exp.name = rule.name;
      if (rule.phase) exp.phase = rule.phase;
      if (rule.seed) exp.seed = rule.seed;
      if (rule.hashVersion) exp.hashVersion = rule.hashVersion;
      if (rule.filters) exp.filters = rule.filters;
      if (rule.condition) exp.condition = rule.condition;

      // Only return a value if the user is part of the experiment
      const {
        result
      } = runExperiment(exp, id, ctx);
      ctx.global.onExperimentEval && ctx.global.onExperimentEval(exp, result);
      if (result.inExperiment && !result.passthrough) {
        return getFeatureResult(ctx, id, result.value, "experiment", rule.id, exp, result);
      }
    }
  }
  process.env.NODE_ENV !== "production" && ctx.global.log("Use default value", {
    id,
    value: feature.defaultValue
  });

  // Fall back to using the default value
  return getFeatureResult(ctx, id, feature.defaultValue === undefined ? null : feature.defaultValue, "defaultValue");
}
export function runExperiment(experiment, featureId, ctx) {
  const key = experiment.key;
  const numVariations = experiment.variations.length;

  // 1. If experiment has less than 2 variations, return immediately
  if (numVariations < 2) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Invalid experiment", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 2. If the context is disabled, return immediately
  if (ctx.global.enabled === false || ctx.user.enabled === false) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Context disabled", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 2.5. Merge in experiment overrides from the context
  experiment = mergeOverrides(experiment, ctx);

  // 2.6 New, more powerful URL targeting
  if (experiment.urlPatterns && !isURLTargeted(ctx.user.url || "", experiment.urlPatterns)) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url targeting", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 3. If a variation is forced from a querystring, return the forced variation
  const qsOverride = getQueryStringOverride(key, ctx.user.url || "", numVariations);
  if (qsOverride !== null) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Force via querystring", {
      id: key,
      variation: qsOverride
    });
    return {
      result: getExperimentResult(ctx, experiment, qsOverride, false, featureId)
    };
  }

  // 4. If a variation is forced in the context, return the forced variation
  const forcedVariations = getForcedVariations(ctx);
  if (key in forcedVariations) {
    const variation = forcedVariations[key];
    process.env.NODE_ENV !== "production" && ctx.global.log("Force via dev tools", {
      id: key,
      variation
    });
    return {
      result: getExperimentResult(ctx, experiment, variation, false, featureId)
    };
  }

  // 5. Exclude if a draft experiment or not active
  if (experiment.status === "draft" || experiment.active === false) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because inactive", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 6. Get the hash attribute and return if empty
  const {
    hashAttribute,
    hashValue
  } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
  if (!hashValue) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because missing hashAttribute", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }
  let assigned = -1;
  let foundStickyBucket = false;
  let stickyBucketVersionIsBlocked = false;
  if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
    const {
      variation,
      versionIsBlocked
    } = getStickyBucketVariation({
      ctx,
      expKey: experiment.key,
      expBucketVersion: experiment.bucketVersion,
      expHashAttribute: experiment.hashAttribute,
      expFallbackAttribute: experiment.fallbackAttribute,
      expMinBucketVersion: experiment.minBucketVersion,
      expMeta: experiment.meta
    });
    foundStickyBucket = variation >= 0;
    assigned = variation;
    stickyBucketVersionIsBlocked = !!versionIsBlocked;
  }

  // Some checks are not needed if we already have a sticky bucket
  if (!foundStickyBucket) {
    // 7. Exclude if user is filtered out (used to be called "namespace")
    if (experiment.filters) {
      if (isFilteredOut(experiment.filters, ctx)) {
        process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of filters", {
          id: key
        });
        return {
          result: getExperimentResult(ctx, experiment, -1, false, featureId)
        };
      }
    } else if (experiment.namespace && !inNamespace(hashValue, experiment.namespace)) {
      process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of namespace", {
        id: key
      });
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 7.5. Exclude if experiment.include returns false or throws
    if (experiment.include && !isIncluded(experiment.include)) {
      process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of include function", {
        id: key
      });
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 8. Exclude if condition is false
    if (experiment.condition && !conditionPasses(experiment.condition, ctx)) {
      process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of condition exp", {
        id: key
      });
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }

    // 8.05. Exclude if prerequisites are not met
    if (experiment.parentConditions) {
      const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
      for (const parentCondition of experiment.parentConditions) {
        ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
        const parentResult = evalFeature(parentCondition.id, ctx);
        // break out for cyclic prerequisites
        if (parentResult.source === "cyclicPrerequisite") {
          return {
            result: getExperimentResult(ctx, experiment, -1, false, featureId)
          };
        }
        const evalObj = {
          value: parentResult.value
        };
        if (!evalCondition(evalObj, parentCondition.condition || {})) {
          process.env.NODE_ENV !== "production" && ctx.global.log("Skip because prerequisite evaluation fails", {
            id: key
          });
          return {
            result: getExperimentResult(ctx, experiment, -1, false, featureId)
          };
        }
      }
    }

    // 8.1. Exclude if user is not in a required group
    if (experiment.groups && !hasGroupOverlap(experiment.groups, ctx)) {
      process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of groups", {
        id: key
      });
      return {
        result: getExperimentResult(ctx, experiment, -1, false, featureId)
      };
    }
  }

  // 8.2. Old style URL targeting
  if (experiment.url && !urlIsValid(experiment.url, ctx)) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 9. Get the variation from the sticky bucket or get bucket ranges and choose variation
  const n = hash(experiment.seed || key, hashValue, experiment.hashVersion || 1);
  if (n === null) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of invalid hash version", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }
  if (!foundStickyBucket) {
    const ranges = experiment.ranges || getBucketRanges(numVariations, experiment.coverage === undefined ? 1 : experiment.coverage, experiment.weights);
    assigned = chooseVariation(n, ranges);
  }

  // 9.5 Unenroll if any prior sticky buckets are blocked by version
  if (stickyBucketVersionIsBlocked) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because sticky bucket version is blocked", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId, undefined, true)
    };
  }

  // 10. Return if not in experiment
  if (assigned < 0) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of coverage", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 11. Experiment has a forced variation
  if ("force" in experiment) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Force variation", {
      id: key,
      variation: experiment.force
    });
    return {
      result: getExperimentResult(ctx, experiment, experiment.force === undefined ? -1 : experiment.force, false, featureId)
    };
  }

  // 12. Exclude if in QA mode
  if (ctx.global.qaMode || ctx.user.qaMode) {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because QA mode", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 12.5. Exclude if experiment is stopped
  if (experiment.status === "stopped") {
    process.env.NODE_ENV !== "production" && ctx.global.log("Skip because stopped", {
      id: key
    });
    return {
      result: getExperimentResult(ctx, experiment, -1, false, featureId)
    };
  }

  // 13. Build the result object
  const result = getExperimentResult(ctx, experiment, assigned, true, featureId, n, foundStickyBucket);

  // 13.5. Persist sticky bucket
  if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
    const {
      changed,
      key: attrKey,
      doc
    } = generateStickyBucketAssignmentDoc(ctx, hashAttribute, toString(hashValue), {
      [getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion)]: result.key
    });
    if (changed) {
      // update local docs
      ctx.user.stickyBucketAssignmentDocs = ctx.user.stickyBucketAssignmentDocs || {};
      ctx.user.stickyBucketAssignmentDocs[attrKey] = doc;
      // save doc
      ctx.user.saveStickyBucketAssignmentDoc(doc);
    }
  }

  // 14. Fire the tracking callback(s)
  // Store the promise in case we're awaiting it (ex: browser url redirects)
  const trackingCalls = onExperimentViewed(ctx, experiment, result);
  if (trackingCalls.length === 0 && ctx.global.saveDeferredTrack) {
    ctx.global.saveDeferredTrack({
      experiment,
      result
    });
  }
  const trackingCall = !trackingCalls.length ? undefined : trackingCalls.length === 1 ? trackingCalls[0] : Promise.all(trackingCalls).then(() => {});

  // 14.1 Keep track of completed changeIds
  "changeId" in experiment && experiment.changeId && ctx.global.recordChangeId && ctx.global.recordChangeId(experiment.changeId);

  // 15. Return the result
  process.env.NODE_ENV !== "production" && ctx.global.log("In experiment", {
    id: key,
    variation: result.variationId
  });
  return {
    result,
    trackingCall
  };
}
function getFeatureResult(ctx, key, value, source, ruleId, experiment, result) {
  const ret = {
    value,
    on: !!value,
    off: !value,
    source,
    ruleId: ruleId || ""
  };
  if (experiment) ret.experiment = experiment;
  if (result) ret.experimentResult = result;

  // Track the usage of this feature in real-time
  if (source !== "override") {
    onFeatureUsage(ctx, key, ret);
  }
  return ret;
}
function getAttributes(ctx) {
  return {
    ...ctx.user.attributes,
    ...ctx.user.attributeOverrides
  };
}
function conditionPasses(condition, ctx) {
  return evalCondition(getAttributes(ctx), condition, ctx.global.savedGroups || {});
}
function isFilteredOut(filters, ctx) {
  return filters.some(filter => {
    const {
      hashValue
    } = getHashAttribute(ctx, filter.attribute);
    if (!hashValue) return true;
    const n = hash(filter.seed, hashValue, filter.hashVersion || 2);
    if (n === null) return true;
    return !filter.ranges.some(r => inRange(n, r));
  });
}
function isIncludedInRollout(ctx, seed, hashAttribute, fallbackAttribute, range, coverage, hashVersion) {
  if (!range && coverage === undefined) return true;
  if (!range && coverage === 0) return false;
  const {
    hashValue
  } = getHashAttribute(ctx, hashAttribute, fallbackAttribute);
  if (!hashValue) {
    return false;
  }
  const n = hash(seed, hashValue, hashVersion || 1);
  if (n === null) return false;
  return range ? inRange(n, range) : coverage !== undefined ? n <= coverage : true;
}
export function getExperimentResult(ctx, experiment, variationIndex, hashUsed, featureId, bucket, stickyBucketUsed) {
  let inExperiment = true;
  // If assigned variation is not valid, use the baseline and mark the user as not in the experiment
  if (variationIndex < 0 || variationIndex >= experiment.variations.length) {
    variationIndex = 0;
    inExperiment = false;
  }
  const {
    hashAttribute,
    hashValue
  } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
  const meta = experiment.meta ? experiment.meta[variationIndex] : {};
  const res = {
    key: meta.key || "" + variationIndex,
    featureId,
    inExperiment,
    hashUsed,
    variationId: variationIndex,
    value: experiment.variations[variationIndex],
    hashAttribute,
    hashValue,
    stickyBucketUsed: !!stickyBucketUsed
  };
  if (meta.name) res.name = meta.name;
  if (bucket !== undefined) res.bucket = bucket;
  if (meta.passthrough) res.passthrough = meta.passthrough;
  return res;
}
function mergeOverrides(experiment, ctx) {
  const key = experiment.key;
  const o = ctx.global.overrides;
  if (o && o[key]) {
    experiment = Object.assign({}, experiment, o[key]);
    if (typeof experiment.url === "string") {
      experiment.url = getUrlRegExp(
      // eslint-disable-next-line
      experiment.url);
    }
  }
  return experiment;
}
export function getHashAttribute(ctx, attr, fallback) {
  let hashAttribute = attr || "id";
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let hashValue = "";
  const attributes = getAttributes(ctx);
  if (attributes[hashAttribute]) {
    hashValue = attributes[hashAttribute];
  }

  // if no match, try fallback
  if (!hashValue && fallback) {
    if (attributes[fallback]) {
      hashValue = attributes[fallback];
    }
    if (hashValue) {
      hashAttribute = fallback;
    }
  }
  return {
    hashAttribute,
    hashValue
  };
}
function urlIsValid(urlRegex, ctx) {
  const url = ctx.user.url;
  if (!url) return false;
  const pathOnly = url.replace(/^https?:\/\//, "").replace(/^[^/]*\//, "/");
  if (urlRegex.test(url)) return true;
  if (urlRegex.test(pathOnly)) return true;
  return false;
}
function hasGroupOverlap(expGroups, ctx) {
  const groups = ctx.global.groups || {};
  for (let i = 0; i < expGroups.length; i++) {
    if (groups[expGroups[i]]) return true;
  }
  return false;
}
function getStickyBucketVariation(_ref) {
  let {
    ctx,
    expKey,
    expBucketVersion,
    expHashAttribute,
    expFallbackAttribute,
    expMinBucketVersion,
    expMeta
  } = _ref;
  expBucketVersion = expBucketVersion || 0;
  expMinBucketVersion = expMinBucketVersion || 0;
  expHashAttribute = expHashAttribute || "id";
  expMeta = expMeta || [];
  const id = getStickyBucketExperimentKey(expKey, expBucketVersion);
  const assignments = getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute);

  // users with any blocked bucket version (0 to minExperimentBucketVersion) are excluded from the test
  if (expMinBucketVersion > 0) {
    for (let i = 0; i <= expMinBucketVersion; i++) {
      const blockedKey = getStickyBucketExperimentKey(expKey, i);
      if (assignments[blockedKey] !== undefined) {
        return {
          variation: -1,
          versionIsBlocked: true
        };
      }
    }
  }
  const variationKey = assignments[id];
  if (variationKey === undefined)
    // no assignment found
    return {
      variation: -1
    };
  const variation = expMeta.findIndex(m => m.key === variationKey);
  if (variation < 0)
    // invalid assignment, treat as "no assignment found"
    return {
      variation: -1
    };
  return {
    variation
  };
}
function getStickyBucketExperimentKey(experimentKey, experimentBucketVersion) {
  experimentBucketVersion = experimentBucketVersion || 0;
  return `${experimentKey}__${experimentBucketVersion}`;
}
export function getStickyBucketAttributeKey(attributeName, attributeValue) {
  return `${attributeName}||${attributeValue}`;
}
function getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute) {
  if (!ctx.user.stickyBucketAssignmentDocs) return {};
  const {
    hashAttribute,
    hashValue
  } = getHashAttribute(ctx, expHashAttribute);
  const hashKey = getStickyBucketAttributeKey(hashAttribute, toString(hashValue));
  const {
    hashAttribute: fallbackAttribute,
    hashValue: fallbackValue
  } = getHashAttribute(ctx, expFallbackAttribute);
  const fallbackKey = fallbackValue ? getStickyBucketAttributeKey(fallbackAttribute, toString(fallbackValue)) : null;
  const assignments = {};
  if (fallbackKey && ctx.user.stickyBucketAssignmentDocs[fallbackKey]) {
    Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[fallbackKey].assignments || {});
  }
  if (ctx.user.stickyBucketAssignmentDocs[hashKey]) {
    Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[hashKey].assignments || {});
  }
  return assignments;
}
function generateStickyBucketAssignmentDoc(ctx, attributeName, attributeValue, assignments) {
  const key = getStickyBucketAttributeKey(attributeName, attributeValue);
  const existingAssignments = ctx.user.stickyBucketAssignmentDocs && ctx.user.stickyBucketAssignmentDocs[key] ? ctx.user.stickyBucketAssignmentDocs[key].assignments || {} : {};
  const newAssignments = {
    ...existingAssignments,
    ...assignments
  };
  const changed = JSON.stringify(existingAssignments) !== JSON.stringify(newAssignments);
  return {
    key,
    doc: {
      attributeName,
      attributeValue,
      assignments: newAssignments
    },
    changed
  };
}
function deriveStickyBucketIdentifierAttributes(ctx, data) {
  const attributes = new Set();
  const features = data && data.features ? data.features : ctx.global.features || {};
  const experiments = data && data.experiments ? data.experiments : ctx.global.experiments || [];
  Object.keys(features).forEach(id => {
    const feature = features[id];
    if (feature.rules) {
      for (const rule of feature.rules) {
        if (rule.variations) {
          attributes.add(rule.hashAttribute || "id");
          if (rule.fallbackAttribute) {
            attributes.add(rule.fallbackAttribute);
          }
        }
      }
    }
  });
  experiments.map(experiment => {
    attributes.add(experiment.hashAttribute || "id");
    if (experiment.fallbackAttribute) {
      attributes.add(experiment.fallbackAttribute);
    }
  });
  return Array.from(attributes);
}
export async function getAllStickyBucketAssignmentDocs(ctx, stickyBucketService, data) {
  const attributes = getStickyBucketAttributes(ctx, data);
  return stickyBucketService.getAllAssignments(attributes);
}
export function getStickyBucketAttributes(ctx, data) {
  const attributes = {};
  const stickyBucketIdentifierAttributes = deriveStickyBucketIdentifierAttributes(ctx, data);
  stickyBucketIdentifierAttributes.forEach(attr => {
    const {
      hashValue
    } = getHashAttribute(ctx, attr);
    attributes[attr] = toString(hashValue);
  });
  return attributes;
}
export async function decryptPayload(data, decryptionKey, subtle) {
  data = {
    ...data
  };
  if (data.encryptedFeatures) {
    try {
      data.features = JSON.parse(await decrypt(data.encryptedFeatures, decryptionKey, subtle));
    } catch (e) {
      console.error(e);
    }
    delete data.encryptedFeatures;
  }
  if (data.encryptedExperiments) {
    try {
      data.experiments = JSON.parse(await decrypt(data.encryptedExperiments, decryptionKey, subtle));
    } catch (e) {
      console.error(e);
    }
    delete data.encryptedExperiments;
  }
  if (data.encryptedSavedGroups) {
    try {
      data.savedGroups = JSON.parse(await decrypt(data.encryptedSavedGroups, decryptionKey, subtle));
    } catch (e) {
      console.error(e);
    }
    delete data.encryptedSavedGroups;
  }
  return data;
}
export function getApiHosts(options) {
  const defaultHost = options.apiHost || "https://cdn.growthbook.io";
  return {
    apiHost: defaultHost.replace(/\/*$/, ""),
    streamingHost: (options.streamingHost || defaultHost).replace(/\/*$/, ""),
    apiRequestHeaders: options.apiHostRequestHeaders,
    streamingHostRequestHeaders: options.streamingHostRequestHeaders
  };
}
export function getExperimentDedupeKey(experiment, result) {
  return result.hashAttribute + result.hashValue + experiment.key + result.variationId;
}
//# sourceMappingURL=core.mjs.map