/**
 * Builder methods for building a collection of search results models for a series of search result data.
 */
import * as validation from "@jet/environment/json/validation";
import { isNothing } from "@jet/environment/types/optional";
import * as models from "../../api/models";
import * as serverData from "../../foundation/json-parsing/server-data";
import { isDataHydrated } from "../../foundation/media/data-structure";
import { shouldFilter } from "../filtering";
import { nextPosition, pushBasicLocation, popLocation } from "../metrics/helpers/location";
import * as onDevicePersonalization from "../personalization/on-device-personalization";
import { searchResultFromData } from "./content/search-results";
import * as searchAds from "./search-ads";
import * as guidedSearch from "./guided-search/guided-search";
import { addImpressionFields } from "../metrics/helpers/impressions";
import { searchAdMissedOpportunityFromId } from "../lockups/ad-lockups";
/**
 * Determines where to display guided search, e.g. as mid-scroll module or pinned to top.
 * @param meta  The metadata from the MAPI response for search catalog request.
 * @param objectGraph The App Store object graph.
 * @returns The position of the mid-scroll guided search module in search results, or `undefined` for the pinned to top experience.
 */
export function guidedSearchPositionFromSearchResponseMeta(meta, objectGraph) {
    var _a, _b, _c;
    // Client debug override
    const guidedSearchPositionOverride = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.integer("GuidedSearchOverrides.position");
    if (serverData.isNumber(guidedSearchPositionOverride) && guidedSearchPositionOverride > 1) {
        return guidedSearchPositionOverride;
    }
    // Server controlled by default
    return (_c = (_b = meta === null || meta === void 0 ? void 0 : meta.displayStyle) === null || _b === void 0 ? void 0 : _b.guidedSearch) === null || _c === void 0 ? void 0 : _c.position;
}
/**
 * Create a `SearchResultsBuildResult` containing set of built and deferred results from the top-level search data container.
 * This method supports prepending adverts.
 * @param objectGraph
 * @param requestMetadata Request metadata for search being performed.
 * @param searchResponseMetadata Response metadata for search that was performed.
 * @param metricsOptions Metrics options for built models.
 * @param resultsData Array of search results data.
 * @param advertData Data container with advert results.
 * @param facetData Array of guided search token response data.
 * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user
 * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past
 */
export async function createSearchResults(objectGraph, requestMetadata, searchResponseMetadata, metricsOptions, resultsData, advertData = undefined, facetData = undefined, installedStates = undefined, appStates = undefined) {
    var _a, _b, _c, _d, _e, _f;
    /// Built models
    const builtResults = [];
    // Unhydrated items that will be fetched in pagination.
    const deferredResults = [];
    // Search Experiments Data
    const searchExperimentsData = searchResponseMetadata || null;
    // Generate the personalization data
    const appIds = resultsData
        .filter((resultData) => {
        return resultData.type === "apps";
    })
        .map((resultData) => {
        return resultData.id;
    });
    const personalizationDataContainer = onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, new Set(appIds));
    // Build Adverts
    let advertsSearchResult;
    let advertsDisplayStyle;
    if (searchAds.platformSupportsAdverts(objectGraph) && serverData.isDefinedNonNullNonEmpty(advertData)) {
        const adsResultAndDisplayStyle = searchAds.adsResultFromSearchResults(objectGraph, advertData, resultsData, requestMetadata, metricsOptions, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, searchExperimentsData, personalizationDataContainer);
        advertsSearchResult = adsResultAndDisplayStyle.result;
        advertsDisplayStyle = adsResultAndDisplayStyle.displayStyle;
        if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) {
            advertsSearchResult.searchAdOpportunity = advertsSearchResult.lockups[0].searchAdOpportunity;
            builtResults.push(advertsSearchResult);
        }
    }
    // Flag for Ad Media Deduping
    let isFirstResult = true;
    const guidedSearchPosition = guidedSearchPositionFromSearchResponseMeta(searchResponseMetadata, objectGraph);
    for (const [index, resultData] of resultsData.entries()) {
        // Inject the mid-scroll guided search module if we've reached the desired position.
        if (index === guidedSearchPosition) {
            const tokens = createGuidedSearchTokens(objectGraph, requestMetadata.requestDescriptor, facetData, metricsOptions);
            if (tokens.length > 0) {
                const title = (_c = (_b = (_a = searchResponseMetadata === null || searchResponseMetadata === void 0 ? void 0 : searchResponseMetadata.displayStyle) === null || _a === void 0 ? void 0 : _a.guidedSearch) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : objectGraph.loc.string("Search.Guided.Title.ExploreMore"); // static fallback query context
                const guidedSearchResult = new models.GuidedSearchResult(title, tokens);
                const impressionOptions = {
                    ...metricsOptions,
                    id: "midScrollGuidedSearch",
                    kind: "grouping",
                    targetType: "module",
                    title: title,
                    softwareType: null,
                };
                addImpressionFields(objectGraph, guidedSearchResult, impressionOptions);
                builtResults.push(guidedSearchResult);
                nextPosition(metricsOptions.locationTracker);
            }
        }
        // Deferred items for subsequent pagination.
        if (!isDataHydrated(resultData)) {
            // On the first unhydrated item, attach the rest of the queue to `deferredResults` to preserve ordering.
            deferredResults.push(...resultsData.slice(index));
            break;
        }
        // Filter
        if (shouldFilter(objectGraph, resultData, 10750 /* Filter.Search */)) {
            continue;
        }
        // Advert: Update CPP data on first organic result.
        // We must do this *after* the ad results are built, because we need to ensure we're picking the first lockup that will appear,
        // not just the first data (that may be filtered somehow).
        if (isFirstResult && serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) {
            searchAds.updateDupeOrganicResultCPPData(objectGraph, advertData !== null && advertData !== void 0 ? advertData : [], advertsSearchResult, resultData, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, metricsOptions, personalizationDataContainer);
        }
        // Build model
        const searchResult = searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, requestMetadata.requestDescriptor.isNetworkConstrained, requestMetadata.requestDescriptor.searchEntity, searchExperimentsData);
        if (!searchResult || !platformSupportsResultType(objectGraph, searchResult)) {
            continue;
        }
        /**
         * Advert: When first advert and result matches, modify media.
         */
        if (isFirstResult &&
            serverData.isDefinedNonNullNonEmpty(advertsSearchResult) &&
            serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) {
            searchAds.dedupeAdMediaFromMatchingResult(objectGraph, advertsSearchResult, searchResult, searchExperimentsData, advertsDisplayStyle);
        }
        /**
         * Advert: When advert isn't available, mark the organic as a missed opportunity slot
         */
        if (isFirstResult &&
            searchAds.platformSupportsAdverts(objectGraph) &&
            serverData.isDefinedNonNull((_d = metricsOptions.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo) &&
            (serverData.isNull(advertsSearchResult) || serverData.isNullOrEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups))) {
            searchResult.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, metricsOptions.pageInformation);
            (_e = searchResult.searchAdOpportunity) === null || _e === void 0 ? void 0 : _e.setMissedOpportunityReason("NOAD");
            (_f = searchResult.searchAdOpportunity) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP");
        }
        builtResults.push(searchResult);
        isFirstResult = false;
        nextPosition(metricsOptions.locationTracker);
    }
    return await applyClientFilteringToIAPs(objectGraph, builtResults).then((builtResultsFiltered) => {
        return {
            builtSearchResults: builtResultsFiltered,
            deferredSearchResults: deferredResults,
        };
    });
}
// endregion
// region Internals
/**
 * Create guided search tokens for the given search request descriptor and facet MAPI response data.
 * @param objectGraph The App Store object graph.
 * @param requestDescriptor The search request descriptor.
 * @param facetData  The media API response for guided search facets.
 * @param metricsOptions The metrics options.
 * @returns The guided search tokens to display.
 */
function createGuidedSearchTokens(objectGraph, requestDescriptor, facetData, metricsOptions) {
    if (!objectGraph.host.isiOS || isNothing(facetData) || facetData.length === 0) {
        return [];
    }
    pushBasicLocation(objectGraph, {
        pageInformation: metricsOptions.pageInformation,
        locationTracker: metricsOptions.locationTracker,
        targetType: "SearchRevisions",
    }, "");
    // Tokens from facet data
    const tokens = [];
    for (const data of facetData) {
        const token = guidedSearch.createGuidedSearchToken(objectGraph, "rewrite", requestDescriptor, data, metricsOptions);
        if (token) {
            tokens.push(token);
            nextPosition(metricsOptions.locationTracker);
        }
    }
    popLocation(metricsOptions.locationTracker);
    return tokens;
}
/**
 * Apply client-side iAP filtering to set of results.
 * @param objectGraph
 * @param resultsToFilter Results to apply iAP filtering on.
 */
async function applyClientFilteringToIAPs(objectGraph, resultsToFilter) {
    return await validation.context("applyClientFilteringToIAPs", async () => {
        const iAPProductIDToParentBundleID = {};
        for (const result of resultsToFilter) {
            if (result.resultType === "inAppPurchase") {
                const inAppPurchaseResult = result;
                const inAppPurchaseLockup = inAppPurchaseResult.lockup;
                if (inAppPurchaseLockup.parent &&
                    inAppPurchaseLockup.productIdentifier &&
                    inAppPurchaseLockup.parent.bundleId) {
                    iAPProductIDToParentBundleID[inAppPurchaseLockup.productIdentifier] =
                        inAppPurchaseLockup.parent.bundleId;
                }
                else {
                    validation.unexpectedNull("ignoredValue", "string", `required fields for ${inAppPurchaseLockup.adamId}`);
                }
            }
        }
        if (Object.keys(iAPProductIDToParentBundleID).length === 0) {
            return await Promise.resolve(resultsToFilter);
        }
        return await objectGraph.clientOrdering.visibilityForIAPs(iAPProductIDToParentBundleID).then((visibilities) => {
            const filteredResults = resultsToFilter.filter((result) => {
                if (result.resultType !== "inAppPurchase") {
                    return true;
                }
                const inAppPurchaseResult = result;
                const inAppPurchaseLockup = inAppPurchaseResult.lockup;
                if (inAppPurchaseLockup.productIdentifier && visibilities[inAppPurchaseLockup.productIdentifier]) {
                    return true;
                }
                else {
                    return inAppPurchaseLockup.isVisibleByDefault;
                }
            });
            return filteredResults;
        });
    });
}
/**
 * Whether or not current platform supports displaying given `searchResult`.
 */
function platformSupportsResultType(objectGraph, searchResult) {
    if (objectGraph.host.isTV) {
        switch (searchResult.resultType) {
            case "content":
            case "editorial":
                return true;
            default:
                return false;
        }
    }
    if (!objectGraph.host.isiOS && !objectGraph.client.isWeb) {
        switch (searchResult.resultType) {
            case "appEvent":
                return false;
            default:
                break;
        }
    }
    return true;
}
// endregion
//# sourceMappingURL=search-results-pipeline.js.map