import * as validation from "@jet/environment/json/validation";
import * as serverData from "../../foundation/json-parsing/server-data";
import * as mediaDataFetching from "../../foundation/media/data-fetching";
import * as mediaNetwork from "../../foundation/media/network";
import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common";
import { Parameters } from "../../foundation/network/url-constants";
export class PersonalizedData {
}
export async function recommendedAppsForUseCase(objectGraph, useCase, displayContext) {
    const displayContextLogString = displayContext === "shelf" ? "OnDeviceRecommendationsShelfController" : "OnDeviceRecommendationsPageController";
    return await new Promise((resolve, reject) => {
        if (!objectGraph.host.isiOS) {
            const errorMessage = `${displayContextLogString}: On device personalization is only enabled on iOS devices.`;
            validation.unexpectedType("defaultValue", errorMessage, null);
            reject(new Error(errorMessage));
            return;
        }
        if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
            const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
            validation.unexpectedType("defaultValue", errorMessage, null);
            reject(new Error(errorMessage));
            return;
        }
        if (serverData.isNullOrEmpty(useCase)) {
            const errorMessage = `${displayContextLogString}: Missing valid useCase for ODP: ${useCase}`;
            validation.unexpectedType("defaultValue", errorMessage, null);
            reject(new Error(errorMessage));
            return;
        }
        objectGraph.onDeviceRecommendationsManager
            .performRequest({
            type: "fetchRecommendations",
            dsId: objectGraph.user.dsid,
            useCase: useCase,
        })
            .then((recoResponse) => {
            const candidates = serverData.asArrayOrEmpty(recoResponse["candidates"]);
            const recoMetrics = serverData.asJSONData(recoResponse["metrics"]);
            if (serverData.isNullOrEmpty(candidates)) {
                const errorMessage = `${displayContextLogString}: ODP returned no candidate ids for useCase: ${useCase}`;
                validation.unexpectedType("defaultValue", errorMessage, null);
                reject(new NoODPCandidatesError(errorMessage));
                return;
            }
            if (serverData.isNullOrEmpty(recoMetrics)) {
                const errorMessage = `${displayContextLogString}: ODP returned no metrics for useCase: ${useCase}`;
                validation.unexpectedType("defaultValue", errorMessage, null);
                reject(new Error(errorMessage));
                return;
            }
            const candidatesData = [];
            for (const candidateId of candidates) {
                if (serverData.isDefinedNonNullNonEmpty(candidateId)) {
                    candidatesData.push({
                        id: candidateId,
                        type: "apps",
                    });
                }
            }
            // MAPI Request
            const mediaApiRequest = new mediaDataFetching.Request(objectGraph, candidatesData)
                .withFilter("apps:recommendable", "true")
                .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase);
            groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest);
            mediaNetwork
                .fetchData(objectGraph, mediaApiRequest)
                .then((recoDataContainer) => {
                resolve({
                    candidates: candidates,
                    recoMetrics: recoMetrics,
                    dataContainer: recoDataContainer,
                });
            })
                .catch((error) => {
                const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for candidates: ${candidates}`;
                validation.unexpectedType("defaultValue", errorMessage, null);
                reject(new Error(errorMessage));
            });
        })
            .catch((error) => {
            const errorMessage = `${displayContextLogString}: Failed to perform ODP for useCase: ${useCase}, ${error}`;
            validation.unexpectedType("defaultValue", errorMessage, null);
            reject(new Error(errorMessage));
        });
    });
}
/***
 * Gives the list of app ids in the order in which they appear in the passed response
 * @param personalizedDataItems The data items to be processed
 * @return List of ordered app ids
 */
export function getOrderedAppIds(personalizedDataItems) {
    const seenAppIds = new Set();
    const orderedAppIds = [];
    personalizedDataItems.forEach((dataItem, index) => {
        if (!seenAppIds.has(dataItem.appId)) {
            orderedAppIds.push(dataItem.appId);
            seenAppIds.add(dataItem.appId);
        }
    });
    return orderedAppIds;
}
/***
 * This function reranks events based on a calculated on-device score
 * that can factor in recency, frequency, or usage of an app
 * @param personalizedDataItems List of personalized data items
 * @param metricsData Metrics data with hyperparameter values to use
 * @return Personalized data items after reranking using on-device signals
 */
export function getUpdatedScoreAfterBoosting(personalizedDataItems, metricsData) {
    var _a;
    const weightParam = Number((_a = metricsData["weight_parameter"]) !== null && _a !== void 0 ? _a : 0.0);
    for (const personalizedDataItem of personalizedDataItems) {
        const serverScore = personalizedDataItem.score;
        const onDeviceScore = personalizedDataItem.onDeviceScore;
        personalizedDataItem.modifiedScore = weightParam * onDeviceScore + (1 - weightParam) * serverScore;
    }
    personalizedDataItems.sort((a, b) => {
        return b.modifiedScore - a.modifiedScore;
    });
    return personalizedDataItems;
}
/***
 * This function will diversify the personalized items on the basis of ordering of apps passed.
 * In case there are pinned items, we will remove them from diversification bucket and keep them in their position and add diversified items in order
 * This can lead to duplicates next to the pinned items. But the pinning and duplicate app events case scenario is rare so we avoid complicated logic here.
 * Explanation ->
 * If the original data items list contains appId in this order -> [1(a), 1(b), 2(a), 3(a), 2(b), 2(c), 3(b), 4(a), 1(c), 4(b)]
 * and the ordering of the apps used for relative ranking is -> [1, 2, 3, 4]
 * Then the updated ordering of the data items should look like -> [1(a), 2(a), 3(a), 4(a), 1(b), 2(b), 3(b), 4(b), 1(c), 2(c)]
 * @param personalizedDataItems List of personalized data items for diversification
 * @param orderedAppIds List of ordered app ids used for relative ordering between in-app events
 * @return The diversified list of data items.
 */
export function diversifyDataItems(personalizedDataItems, orderedAppIds) {
    const pinnedItems = personalizedDataItems.filter((item) => serverData.isDefinedNonNull(item.pinnedPosition));
    personalizedDataItems = personalizedDataItems.filter((item) => serverData.isNull(item.pinnedPosition));
    const appIdGroups = new Map();
    // Insert the elements in a map in reverse order of their appearance in dataItems so that we can later use pop() instead of shift()
    // 1 -> [1(c), 1(b), 1(a)], 2 -> [2(c), 2(b), 2(a)], 3 -> [3(b), 3(a)], 4-> [4(b), 4(a)]
    personalizedDataItems.reverse();
    personalizedDataItems.forEach((dataItem, index) => {
        if (dataItem.appId in appIdGroups) {
            appIdGroups[dataItem.appId].push(dataItem);
        }
        else {
            appIdGroups[dataItem.appId] = [dataItem];
        }
    });
    const diversifiedDataItems = [];
    // We find the max number of appItems for our appIds, to determine the iteration count
    const maxAppEventsForAppId = Math.max(...Object.values(appIdGroups).map((a) => a.length));
    for (let index = 0; index < maxAppEventsForAppId; index++) {
        const reducedServerSideAppIdsOrdering = [];
        orderedAppIds.forEach((appId) => {
            if (appId in appIdGroups && appIdGroups[appId].length > 0) {
                diversifiedDataItems.push(appIdGroups[appId].pop());
                if (appIdGroups[appId].length !== 0) {
                    reducedServerSideAppIdsOrdering.push(appId);
                }
            }
        });
        orderedAppIds = reducedServerSideAppIdsOrdering;
    }
    // Merge the pinned items and diversified items
    const finalizedResponse = new Array(pinnedItems.length + diversifiedDataItems.length);
    // Sort pinned items by pinned position to be able to handle the pinned items that exceed the final list length
    // For all the pinned items that have position greater than input list size, we just add them to end of the list
    pinnedItems.sort((a, b) => a.pinnedPosition - b.pinnedPosition);
    for (const dataItem of pinnedItems) {
        if (dataItem.pinnedPosition < finalizedResponse.length) {
            // Possible edgecase - Sanity check in case filtering reduces the length of items
            finalizedResponse[dataItem.pinnedPosition] = dataItem;
        }
        else {
            // Extremely rare case scenario: Push these items to diversifiedDataItemsList
            diversifiedDataItems.push(dataItem);
        }
    }
    diversifiedDataItems.reverse(); // To allow popping for first element fetch
    for (const [index, finalizedItem] of finalizedResponse.entries()) {
        if (serverData.isNull(finalizedItem) && diversifiedDataItems.length) {
            finalizedResponse[index] = diversifiedDataItems.pop();
        }
    }
    return finalizedResponse;
}
export class NoODPCandidatesError extends Error {
}
//# sourceMappingURL=on-device-recommendations-common.js.map