/**
 * Builds objects for Product Page Variants (PPV)
 *
 * PPV has two components:
 * - Custom Product Pages:          Special versions of product pages with different metadata (e.g. artwork).
 * - Product Page Treatments:       A/B testing treatments for icon, artwork, etc.
 *
 * At a high level:
 * - Multiple versions of product pages now exist (Default and Custom Pages)
 * - Each version of product page **may** have a special A/B testing treatment determined by xp_ab testing cookie.
 *
 * Example page configurations are:
 * - Default page with no treatment.
 * - Default page with treatment B
 * - Custom page A with treatment C.
 *
 * There are two signals that determine which variant to show:
 * - `meta.cppData`:            Describes which CPP to show / link to. May be explicitly requested via `ppid` param, or automagically programmed.
 * - `meta.experimentData`:     Describes which experiments are running, so client can select the correct treatment.
 *
 * Based on contents of above, we choose which asset to use for given model. As a rule, we use assets in the following order:
 * 1. Assets for CPP (if any)
 * 2. Assets for Treatment (if any)
 * 3. Default Assets
 * i.e. CPP overrides Treatments overrides Default
 *
 * For 2021, scope is limited to:
 * - iOS only
 * - CPPs don't have treatments (though we don't care at our layer)
 */
import { asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, traverse, } from "../../foundation/json-parsing/server-data";
import { unreachable } from "../../foundation/util/errors";
import { contentAttributeAsDictionary } from "../content/attributes";
import { productVariantTreatmentId } from "../../foundation/experimentation/product-page-experiments";
import { isNothing, isSome } from "@jet/environment/types/optional";
// region API
/**
 * Whether or not custom attributes data should be fetched. This is effectively a feature gate.
 * Downstream builder code should parameterize based presence of custom attributes within response, not this flag.
 */
export function shouldFetchCustomAttributes(objectGraph) {
    // Custom attributes are used on web or for PPV (iOS Only)
    return (objectGraph.client.isWeb || objectGraph.host.isiOS) && objectGraph.bag.enableProductPageVariants;
}
/**
 * Returns the app variant data for given `data` and `treatmentGroupId`
 * @param data The data for app resource
 * @param treatmentGroupId The treatment group to retrieve variant for. (For testing)
 */
export function productVariantDataForData(objectGraph, data, treatmentGroupId) {
    if (treatmentGroupId === undefined) {
        treatmentGroupId = getClientTreatmentGroupId(objectGraph);
    }
    if (isNullOrEmpty(data.id)) {
        return undefined;
    }
    const variantData = {
        adamID: data.id,
        productPageId: defaultIdentifier,
        treatmentPageIdMap: { [defaultIdentifier]: defaultIdentifier },
        experimentIdMap: {},
        experimentLocaleMap: {},
    };
    if (isNullOrEmpty(traverse(data, "meta", null))) {
        return variantData; // skip if `meta` is empty.
    }
    /**
     * Determine productPageId (CPP or default)
     */
    copyCustomProductPageData(objectGraph, variantData, data);
    /**
     * Determine treatmentPageId or `default` pageId
     *
     * # Why default, and not `variantData.productPageId` (which can be a CPP id?)
     * - At product-level, CPPs DON'T support AB Testing Treatments today.
     */
    copyExperimentPageData(objectGraph, variantData, defaultIdentifier, treatmentGroupId, data);
    if (!preprocessor.PRODUCTION_BUILD && objectGraph.client.isiOS) {
        if (objectGraph.bag.enableAdditionalLoggingForPPV) {
            objectGraph.console.log(`[PPV] productVariantDataForData: id: ${data.id} productPageId: ${variantData.productPageId} treatmentPageId[default]: ${variantData.treatmentPageIdMap[defaultIdentifier]}`);
        }
    }
    return variantData;
}
/**
 * Copy custom product page data into `ProductVariantData` for given MAPI apps resource.
 * @param objectGraph Object graph
 * @param variantData Variant data to copy CPP data to
 * @param data Apps resource data to source CPP data from
 */
function copyCustomProductPageData(objectGraph, variantData, data) {
    const cppData = traverse(data, "meta.cppData", null);
    if (isNullOrEmpty(cppData)) {
        return;
    }
    // MAPI checks the validity of the ppid provided, so if it exists, always use it.
    const customProductPageId = asString(cppData, "ppid");
    if (isDefinedNonNullNonEmpty(customProductPageId)) {
        variantData.productPageId = customProductPageId;
    }
}
/**
 * Copy treatment page data into `ProductVariantData` for a specified `pageId`.
 * @param objectGraph Object graph
 * @param variantData Variant data to copy experiment data to
 * @param pageId The page id to determine treatment for. (Treatments are unique to each page id)
 * @param data Apps resource data to source experiment data from
 */
function copyExperimentPageData(objectGraph, variantData, pageId, treatmentGroupId, data) {
    const experimentData = traverse(data, "meta.experimentData", null);
    if (isNullOrEmpty(experimentData)) {
        return;
    }
    // Evaluate for specified `pageId`, which may **not** match `variantData.productPageId`
    const pageExperimentData = traverse(experimentData, pageId, null);
    if (isNullOrEmpty(pageExperimentData)) {
        return;
    }
    const experimentId = asString(pageExperimentData, "experimentId");
    const experimentLocale = asString(pageExperimentData, "experimentLocale");
    let treatmentPageId = null;
    const trafficAllocation = traverse(pageExperimentData, "trafficAllocation", null);
    if (isDefinedNonNullNonEmpty(trafficAllocation)) {
        // Nonpersonalized endpoint. Resolve treatment from traffic allocation
        treatmentPageId = matchingTreatmentPageIdFromTrafficAllocation(objectGraph, trafficAllocation, treatmentGroupId);
    }
    else {
        // Personalized endpoint. Infer treatment from thinned variation.
        treatmentPageId = matchingTreatmentPageIdFromThinnedCustomAttributes(objectGraph, data, pageId);
    }
    if (isDefinedNonNullNonEmpty(treatmentPageId) && isDefinedNonNullNonEmpty(experimentId)) {
        variantData.treatmentPageIdMap[pageId] = treatmentPageId;
        variantData.experimentIdMap[pageId] = experimentId;
        if (isDefinedNonNullNonEmpty(experimentLocale)) {
            variantData.experimentLocaleMap[pageId] = experimentLocale;
        }
    }
}
/**
 * Selects the variant custom attribute for given key in custom attributes json.
 *
 * For a given attribute key, e.g. `customArtwork`, and a variant data specifying which product page id and treatment id,
 * it looks in the following locations in priority order:
 * - customAttributes.{productPageId}.{treatmentPageId}
 * - customAttributes.{productPageId}.default
 * - customAttributes.default.{treatmentPageId}
 * - customAttributes.default.default.
 *
 * This effectively means:
 * - Custom Product Page w/ AB Treatment.
 * - Custom Product Page w/ default treament
 * - Default Product Page w/ AB Treatment.
 * - Default Product Page w/ default treament
 *
 * @param objectGraph Dependency cocktail
 * @param customAttributes A `customAttributes` JSON attribute dict. This is a field in a specific platform attribute, e.g. `platformAttributes.ios.customAttributes`
 * @param productVariantData Specifies product page id and treatment id to use.
 * @param attributeKey The attribute to find
 * @param allowNondefaultTreatmentInNondefaultPage Whether or not to use nondefault treatment can be fetched for nondefault page. Used to limit AB Testing effects on CPPs
 * @returns `JSONValue` containing attribute for given key, or `null`
 */
export function variantAttributeForKey(objectGraph, customAttributes, productVariantData, attributeKey, allowNondefaultTreatmentInNondefaultPage) {
    if (isNullOrEmpty(customAttributes)) {
        return null;
    }
    // Contains a set of search paths for custom attributes by priority
    let searchPaths;
    if (productVariantData.productPageId !== defaultIdentifier) {
        /**
         * `productPageId` is nondefault (CPP). Priorities are:
         * - CPP Page Data
         * - AB Treatment on Default Page (may be default treatment) IF attribute allows AB testing on CPP (allowNondefaultTreatmentInNondefaultPage).
         * - Default Treatment on Default Page
         *
         * Note that treatment for `productPageId` is skipped today.
         */
        const treatmentForDefault = productVariantData.treatmentPageIdMap[defaultIdentifier];
        if (allowNondefaultTreatmentInNondefaultPage) {
            // Allow default.treatment after cpp search path.
            searchPaths = [
                `${productVariantData.productPageId}.${defaultIdentifier}.${attributeKey}`,
                `${defaultIdentifier}.${treatmentForDefault}.${attributeKey}`,
                `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`,
            ];
        }
        else {
            // Skip default.treatment after cpp search path.
            searchPaths = [
                `${productVariantData.productPageId}.${defaultIdentifier}.${attributeKey}`,
                `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`,
            ];
        }
    }
    else {
        /**
         * `productPageId` is default. Priorities are:
         * - AB Treatment on Default Page (may be default)
         * - Default Treatment on Default Page
         */
        const treatmentForDefault = productVariantData.treatmentPageIdMap[defaultIdentifier];
        searchPaths = [
            `${defaultIdentifier}.${treatmentForDefault}.${attributeKey}`,
            `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`,
        ];
    }
    for (const path of searchPaths) {
        const variantAttribute = traverse(customAttributes, path);
        if (isDefinedNonNull(variantAttribute)) {
            // `variantAttribute` can be an "empty" override, e.g. no screenshots or no video.
            return variantAttribute;
        }
    }
    return null;
}
/**
 * Extract the product variant ID (a.k.a. `ppid`, a.k.a. Custom Product Page Identiier) from `ProductVariantData`
 *
 * # Product Variant ID v.s. `productPageId`
 * Product Variant ID is the canonical value for `ppid` query param, used to **request** custom variants for apps resource.
 * `productPageId` may be equal to product page id, except for when `productPageId` is default which indicates a lack of product variant id.
 *
 * @param productVariantData The data to get data for.
 * @returns `string` or `null` for the product variant id
 * @seealso customProductPageIdForData
 */
export function productVariantIDForVariantData(productVariantData) {
    // Default is not a valid variant ID.
    if (isNothing(productVariantData) || productVariantData.productPageId === defaultIdentifier) {
        return null;
    }
    return productVariantData.productPageId;
}
/**
 * Convenience function to retrieve the custom product page id for given `Data` directly
 * @param objectGraph Object graph
 * @param data The data to get custom product page id for.
 * @seealso productVariantIDForVariantData
 */
export function customProductPageIdForData(objectGraph, data) {
    const variantData = productVariantDataForData(objectGraph, data);
    return productVariantIDForVariantData(variantData);
}
/**
 * Extract all available product variant IDs (a.k.a. `ppId` or `cppId`) from the given app data.
 *
 * @param objectGraph Dependencies all the way down.
 * @param data The app data from which to get the available product variant IDs.
 * @returns `string[]` for the available product variant ids, or null if there are none.
 */
export function allProductVariantIdsForData(objectGraph, data) {
    const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes");
    if (customAttributes === null || isNullOrEmpty(customAttributes)) {
        return null;
    }
    const keys = Object.keys(customAttributes);
    const allProductVariantIds = keys.filter((key) => key !== defaultIdentifier);
    return allProductVariantIds;
}
/**
 * Determines the treatment of product page to use for given data from a trafficAllocation json.
 * This is used on unpersonalized endpoints, where client must resolve traffic allocation manually.
 * @param trafficAllocation The traffic allocation for a specific page.
 * @param treatmentGroupId The treatment group to retrieve treatment page id for, e.g. "5". This is usually provided by a cookie.
 */
function matchingTreatmentPageIdFromTrafficAllocation(objectGraph, trafficAllocation, treatmentGroupId) {
    if (treatmentGroupId === undefined || isNullOrEmpty(treatmentGroupId)) {
        return defaultIdentifier; // Default if no AB bucket.
    }
    /**
     * Iterate over traffic allocation map that looks like:
     * "85b6a82c-43e6-11eb-b378-0242ac130002": ["1", "19", "51", ...]
     * "43de034f-43e6-11eb-b378-0242ac130002": ["2", "34", "55", ...]
     */
    for (const [treatmentPageId, includedTreatmentGroups] of Object.entries(trafficAllocation)) {
        if (isDefinedNonNullNonEmpty(includedTreatmentGroups) &&
            includedTreatmentGroups.indexOf(treatmentGroupId) !== -1) {
            return treatmentPageId;
        }
    }
    return defaultIdentifier;
}
/**
 * Determines the treatment of product page to use for given data based on thinned custom attributes.
 * This is used on personalized endpoints, where server can thin the response.
 * @param pageExperimentData The experiment data specific a page variant
 * @param treatmentGroupId The treatment group to retrieve treatment page id for, e.g. "5". This is usually provided by a cookie.
 */
function matchingTreatmentPageIdFromThinnedCustomAttributes(objectGraph, data, pageId) {
    // Traverse keys on `customAttributes` to find treatment page id.
    const customAttributesForPage = contentAttributeAsDictionary(objectGraph, data, `customAttributes.${pageId}`);
    if (isNullOrEmpty(customAttributesForPage)) {
        return defaultIdentifier;
    }
    const treatmentPageId = Object.keys(customAttributesForPage)[0];
    if (isNullOrEmpty(treatmentPageId)) {
        return defaultIdentifier;
    }
    return treatmentPageId;
}
// endregion
// region Treatment Group ID
/**
 * Retrieve the treatment group id, which is a identifier for a traffic bucket the client belongs in.
 */
function getClientTreatmentGroupId(objectGraph) {
    /**
     * Use xp_ab cookie value
     */
    const treatmentId = productVariantTreatmentId(objectGraph);
    if (!preprocessor.PRODUCTION_BUILD) {
        if (lastSeenClientTreatmentId !== treatmentId) {
            objectGraph.console.log("[PPV] Treatment Group ID", treatmentId);
            lastSeenClientTreatmentId = treatmentId;
        }
    }
    return treatmentId;
}
// endregion
// region API - Metrics
/**
 * Builds the metrics dictionary to add to page fields for a software page w/ product variant features (CPP, AB Testing)
 * @param productVariantData The variant data of **page** to build page fields for.
 */
export function pageFieldsForPageInfoProductVariantData(productVariantData) {
    const fields = {};
    /**
     * Custom Product Page Fields
     */
    if (productVariantDataHasVariant(productVariantData, "customProductPage")) {
        fields["pageCustomId"] = productVariantData.productPageId;
    }
    /**
     * AB Testing Page Fields.
     * Always from "default" (instead of productVariantData.productPageId).
     * CPPs don't support AB testing.
     */
    const treatmentPageId = productVariantData.treatmentPageIdMap[defaultIdentifier];
    const experimentId = productVariantData.experimentIdMap[defaultIdentifier];
    const experimentLocale = productVariantData.experimentLocaleMap[defaultIdentifier];
    if (productVariantDataHasVariant(productVariantData, "abExperiment")) {
        fields["pageVariantId"] = treatmentPageId;
        fields["pageExperimentId"] = experimentId;
        fields["pageExperimentLocale"] = experimentLocale;
    }
    return fields;
}
/**
 * Builds content field included in impressions and locations metrics for a specific product variant.
 * @param productVariantData The variant data of impressionable data to build impression fields for.
 */
export function contentFieldsForProductVariantData(productVariantData) {
    const fields = {};
    /**
     * Custom Product Page Content Fields.
     * Used to describe the Custom Product Page data being presented in a lockup,
     * and the Custom Product Page the lockup points to.
     */
    if (productVariantDataHasVariant(productVariantData, "customProductPage")) {
        fields["pageCustomId"] = productVariantData.productPageId;
    }
    /**
     * AB Testing Content Fields.
     * Always from "default" attributes (instead of productVariantData.productPageId).
     * CPPs don't support AB testing.
     */
    const treatmentPageId = productVariantData.treatmentPageIdMap[defaultIdentifier];
    const experimentId = productVariantData.experimentIdMap[defaultIdentifier];
    const experimentLocale = productVariantData.experimentLocaleMap[defaultIdentifier];
    if (productVariantDataHasVariant(productVariantData, "abExperiment")) {
        fields["variantId"] = treatmentPageId;
        fields["experimentId"] = experimentId;
        fields["experimentLocale"] = experimentLocale;
    }
    return fields;
}
/**
 * Update `buyParams` with PPV metrics fields from the page and content's product variant data
 *
 * # Why are there two product variant data?
 * Consider the following scenario:
 * - On Page for App A that has AB tests
 * - The page features App B, which also has AB Tests.
 * On the given page, there are two AB tests occuring in the page. In the `PurchaseConfiguration` for App A and App B, there are two product variant captured:
 * - Page Product Variant Data for App A (Since we're on App A's Product Page)
 * - Item Product Variant Data for App A or B, for App A and B respectively.
 *
 * @param buyParams The buyParam to add product page variant metrics to
 * @param adamID The adam id of item being purchased.
 * @param pageProductVariantData The product variant data for **PAGE** the purchase is occuring on. May be the same as `itemProductVariantData`. May be undefined.
 * @param itemProductVariantData The product variant data for **ITEM** being purchased. May be undefined.
 */
export function addProductPageVariantMetricsToBuyParams(buyParams, adamID, pageProductVariantData, itemProductVariantData) {
    /**
     * Only add certain page / fields of fields if data is present, adam id matches, and information isn't redundant.
     */
    const addPageFields = isDefinedNonNull(pageProductVariantData) && pageProductVariantData.adamID === adamID;
    const addContentFields = isDefinedNonNull(itemProductVariantData) && itemProductVariantData.adamID === adamID && !addPageFields; // if we're adding page fields, don't add item variant fields
    // Product variant data of **PAGE** to buy params.
    if (addPageFields && isDefinedNonNull(pageProductVariantData)) {
        const productVariantPageFields = pageFieldsForPageInfoProductVariantData(pageProductVariantData);
        for (const key of Object.keys(productVariantPageFields)) {
            const value = asString(productVariantPageFields, key);
            if (isSome(value)) {
                buyParams.set(key, value);
            }
        }
    }
    // Product variant data of **ITEM** within a page to buy params.
    if (addContentFields && isDefinedNonNull(itemProductVariantData)) {
        const productVariantFields = contentFieldsForProductVariantData(itemProductVariantData);
        for (const key of Object.keys(productVariantFields)) {
            const value = asString(productVariantFields, key);
            if (isSome(value)) {
                buyParams.set(key, value);
            }
        }
    }
}
/**
 * Whether or not product variant data has an custom product page / ab testing variants for it.
 * While we always create a `ProductVariantData` to parse the attributes, this is used to determine if the data actually has
 * an developer-supplied variant for custom product pages or ab testing experiment from a content perspective.
 *
 * @param productVariantData The product variant data to check.
 * @param variantType The type of variation to query if product variant actually exist for.
 * @returns Whether or not specified variant exists for content.
 */
export function productVariantDataHasVariant(productVariantData, variantType) {
    if (isNull(productVariantData)) {
        return false;
    }
    switch (variantType) {
        case "customProductPage":
            return (isDefinedNonNullNonEmpty(productVariantData.productPageId) &&
                productVariantData.productPageId !== defaultIdentifier);
        case "abExperiment":
            return isDefinedNonNullNonEmpty(productVariantData.experimentIdMap);
        default:
            unreachable(variantType);
    }
}
/**
 * Convenience API for `productVariantDataHasVariant`
 * @param objectGraph Dependency soup
 * @param data The apps resource to determine whether a variant is present for
 * @param variantType The type of variation to query if product variant actually exist for.
 * @returns Whether or not specified variant exists for content.

 */
export function appDataHasVariant(objectGraph, data, variantType) {
    const variantData = productVariantDataForData(objectGraph, data);
    if (isNothing(variantData)) {
        return null;
    }
    return productVariantDataHasVariant(variantData, variantType);
}
// endregion
// region Requests
/**
 * Add PPV specific query parameters necessary to fetch the correct variant in subsequent requests for unhydrated items.
 * Specifically this adds:
 *      &ppid[apps:<appid>]=<cppid>
 * for every `Data` resource that had a non-default variant identifier.
 *
 * This should be called for every `Request` initializer that passes a `Data[]`, found via "new .*Request\(.*s\)" regex.
 *
 * ## Why does this not live in `Request` initializer?
 * It would be ideal to handle this in the initializer for `Request` that accepts `Data[]` (and extracts `ids`), but
 * `Request` lives in `foundation` and (tries) to not have feature specific logic, just URL abstractions.
 * Adding CPP ID determining logic in `Request` constructor violates dependency rules.
 * This can be solved by another layer of abstraction (see `BaseRequest`), but most requests use `Requests` directly today.
 *
 * @param request The request to modify.
 * @param items Items to add PPV query params for.
 */
export function addVariantParametersToRequestForItems(objectGraph, request, items) {
    /**
     * For catalog requets for `Data[]`, we also need to specify custom variant if it was initially vended with one:
     * so the same custom variant is fetched
     */
    for (const item of items) {
        const cppId = customProductPageIdForData(objectGraph, item);
        if (isDefinedNonNull(cppId)) {
            request.addingQuery(`ppid[apps:${item.id}]`, `${cppId}`);
        }
    }
}
// endregion
// region Constants
/**
 * A constant to designate a `default` productPageId or treatmentPageId identifier.
 */
const defaultIdentifier = "default";
/**
 * Last seen client treatment group id for logging.
 */
let lastSeenClientTreatmentId;
// endregion Constants
//# sourceMappingURL=product-page-variants.js.map