import { isNothing, isSome } from "@jet/environment";
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 { Parameters } from "../../foundation/network/url-constants";
import { allOptional } from "../../foundation/util/promise-util";
import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common";
import * as lottery from "../util/lottery";
import { startPromiseWithAdditionalTimeout } from "../util/timeout-manager-util";
import { isPersonalizationAvailable } from "./on-device-personalization";
export const todayTabODPTimeoutUseCase = "todayTabPersonalization";
const displayContextLogString = "OnDeviceRecommendationsTodayShelfController";
/**
 * Convenience function for determining if Today tab Arcade personalization is available.
 */
export function isTodayTabArcadePersonalizationAvailable(objectGraph) {
    const isDataPersonalizationAvailable = isPersonalizationAvailable(objectGraph);
    const isiOS = objectGraph.client.isiOS;
    const isFeatureEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.todayTabArcadePersonalizationRate);
    // Personalization is enabled only IF
    // - Data personalization is available AND
    // - Client is iOS AND
    // - Feature is enabled for current user
    return isDataPersonalizationAvailable && isiOS && isFeatureEnabledForCurrentUser;
}
/**
 * Fetches and returns Today recommendations result using on-device recommendations manager with timeout.
 * @param timeout: Optional timeout in seconds.
 * @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result.
 */
export async function fetchTodayRecommendationsWithTimeout(objectGraph, timeout) {
    return await startPromiseWithAdditionalTimeout(objectGraph, fetchTodayRecommendations(objectGraph), timeout, todayTabODPTimeoutUseCase);
}
/**
 * Fetches and returns Today recommendations result using on-device recommendations manager.
 * @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result.
 */
async function fetchTodayRecommendations(objectGraph) {
    try {
        const useCases = await fetchUseCasesForTab("today", objectGraph);
        const recommendationPromises = useCases.map(async (useCase) => await fetchTodayRecommendationForUseCase(objectGraph, useCase));
        const recommendationPromiseResults = await allOptional(recommendationPromises);
        const recommendations = recommendationPromiseResults
            .map((promiseResult) => {
            if (promiseResult.success) {
                return promiseResult.value;
            }
            else {
                return undefined;
            }
        })
            .filter(isSome);
        return new TodayRecommendationsResult(recommendations);
    }
    catch (error) {
        objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP for Today recommendations: ${error}`);
        return undefined;
    }
}
/**
 * Fetches Today Recommendation for a use case using on-device recommendations manager.
 * - useCase: On-device personalization use case.
 * @returns Promise<TodayRecommendation | undefined>: Today recommendation.
 */
async function fetchTodayRecommendationForUseCase(objectGraph, useCase) {
    try {
        const odrResponse = await fetchTodayRecommendation(useCase, objectGraph);
        const recommendedCandidatesAndMetrics = await makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse);
        const recoMetrics = recommendedCandidatesAndMetrics.metrics;
        const recommendedCandidates = recommendedCandidatesAndMetrics.candidates;
        if (recommendedCandidates.length === 0) {
            return undefined;
        }
        // Select the first candidate as we support only one candidate for each use case.
        const recommendedCandidate = recommendedCandidates[0];
        const todayRecommendationPromise = await makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph);
        return todayRecommendationPromise;
    }
    catch (error) {
        objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`);
        return undefined;
    }
}
/**
 * Fetches and returns use cases for given tab using on-device recommendations manager.
 * - tab: Navigation tab.
 * @returns Promise<string[]>: Use cases for given tab.
 */
export async function fetchUseCasesForTab(tab, objectGraph) {
    if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
        const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
    try {
        const odrResponse = await objectGraph.onDeviceRecommendationsManager.performRequest({
            type: "fetchUseCases",
            tab: tab,
            dsId: objectGraph.user.dsid,
        });
        const useCases = serverData.asArrayOrEmpty(odrResponse["useCases"]);
        if (serverData.isNullOrEmpty(useCases)) {
            const errorMessage = `${displayContextLogString}: ODP returned no use cases for tab: ${tab}`;
            validation.unexpectedType("defaultValue", errorMessage, null);
            throw new Error(errorMessage);
        }
        return useCases;
    }
    catch (error) {
        const errorMessage = `${displayContextLogString}: Failed to fetch ODP use cases for tab: ${tab}, with error: ${error}`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
}
/**
 * Fetches and returns Today recommendation response for given use case using on-device recommendations manager.
 * - useCase: Use case.
 * @returns Promise<JSONData>: Today recommendation response.
 */
export async function fetchTodayRecommendation(useCase, objectGraph) {
    if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
        const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
        throw new Error(errorMessage);
    }
    try {
        return await objectGraph.onDeviceRecommendationsManager.performRequest({
            type: "fetchRecommendations",
            dsId: objectGraph.user.dsid,
            useCase: useCase,
        });
    }
    catch (error) {
        const errorMessage = `${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
}
/**
 * Makes and returns Today recommended candidates and metrics from given useCase and on-device recommendation response.
 * - useCase: Use case.
 * - odrResponse: On-device recommendation response.
 * @returns { candidates: TodayRecommendedCandidate[]; metrics: JSONData }: Today recommended candidates and reco metrics.
 */
export async function makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse) {
    const recoCandidates = serverData.asArrayOrEmpty(odrResponse["candidates"]);
    if (serverData.isNullOrEmpty(recoCandidates)) {
        const errorMessage = `${displayContextLogString}: ODP returned no candidates for useCase: ${useCase}`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
    const recoMetrics = serverData.asJSONData(odrResponse["metrics"]);
    const recommendedCandidates = recoCandidates.map((candidate) => makeRecommendedCandidate(candidate)).filter(isSome);
    if (serverData.isNull(recoMetrics) || serverData.isNullOrEmpty(recommendedCandidates)) {
        const errorMessage = `${displayContextLogString}: ODP candidates could not be parsed for useCase: ${useCase}`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
    return { candidates: recommendedCandidates, metrics: recoMetrics };
}
/**
 * Makes and returns Today recommendation for given use case and candidates using MAPI response.
 * - useCase: Use case.
 * - recommendedCandidate: Recommended candidate.
 * - recoMetrics: Reco metrics.
 * @returns Promise<TodayRecommendation>: Today recommendation.
 */
export async function makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph) {
    const mediaApiRequest = new mediaDataFetching.Request(objectGraph, recommendedCandidate.data, true)
        .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase)
        .addingQuery(Parameters.filterRecommendable, "true");
    groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest);
    try {
        const mapiResponse = await mediaNetwork.fetchData(objectGraph, mediaApiRequest);
        return new TodayRecommendation(useCase, [recommendedCandidate], recoMetrics, mapiResponse);
    }
    catch (error) {
        const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for: ${recommendedCandidate.data}, with error: ${error}`;
        validation.unexpectedType("defaultValue", errorMessage, null);
        throw new Error(errorMessage);
    }
}
/**
 * Makes and returns Today recommended candidate using given candidate data.
 * - candidate: Candidate data.
 * @returns TodayRecommendedCandidate: Today recommended candidate.
 */
export function makeRecommendedCandidate(candidate) {
    // Extract candidate ID and type from candidate data.
    const candidateID = serverData.asString(candidate.id);
    const candidateType = serverData.asString(candidate.type);
    if (serverData.isNull(candidateID)) {
        return undefined;
    }
    let storyIDs = [];
    let mediaType;
    switch (candidateType) {
        case "editorialItemGroup":
            // Exract story candidates to use within the story group.
            const storyCandidates = serverData.asArrayOrEmpty(candidate.candidates);
            // Create story IDs from story candidates.
            storyIDs = storyCandidates
                .map((storyCandidate) => serverData.asString(storyCandidate.id))
                .filter((storyID) => isSome(storyID));
            mediaType = "editorial-item-groups";
            break;
        case "editorialItem":
            mediaType = "editorial-items";
            break;
        default:
            return undefined;
    }
    let candidatesData = [];
    candidatesData.push({
        id: candidateID,
        type: mediaType,
    });
    // If there are any story IDs, add story candidates to candidates data.
    if (serverData.isDefinedNonNullNonEmpty(storyIDs)) {
        const storyCandidates = storyIDs.map((storyID) => ({
            id: storyID,
            type: "editorial-items",
        }));
        candidatesData = candidatesData.concat(storyCandidates);
    }
    return new TodayRecommendedCandidate(candidateID, mediaType, storyIDs, candidatesData);
}
/**
 * A container for Today recommendations that also makes story data and story group data.
 */
export class TodayRecommendationsResult {
    /**
     * Initializes today recommendations results.
     * @param recommendations - Today recommendations.
     */
    constructor(recommendations) {
        this.recommendations = recommendations;
    }
    /**
     * Returns story data from recommendations that has the given use case.
     * @param useCase - Use case that is used to match a story.
     * @returns Story data for given use case and type, or null if not found.
     */
    storyData(useCase) {
        var _a;
        const recommendation = this.recommendationForUseCase(useCase);
        const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-items");
        if (isNothing(recommendation) || isNothing(candidate)) {
            return undefined;
        }
        return (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === candidate.id);
    }
    /**
     * Returns story group data from recommendations that has the given use case.
     * @param useCase - Use case that is used to match a story group.
     * @returns Story group data for given use case and type, or null if not found.
     */
    storyGroupData(useCase) {
        var _a, _b;
        const recommendation = this.recommendationForUseCase(useCase);
        const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-item-groups");
        if (isNothing(recommendation) || isNothing(candidate)) {
            return undefined;
        }
        const storyGroupData = (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === (candidate === null || candidate === void 0 ? void 0 : candidate.id));
        const storiesData = (_b = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _b === void 0 ? void 0 : _b.data.filter((item) => candidate.candidateIDs.includes(item.id));
        if (isNothing(storyGroupData) || isNothing(storiesData)) {
            return undefined;
        }
        storyGroupData["meta"] = {
            associations: {
                recommendations: {
                    data: storiesData,
                },
            },
        };
        return storyGroupData;
    }
    /**
     * Returns the first recommendation that has the given use case.
     * @param useCase - Use case that is used to match a recommendation.
     * @returns Recommendation for given use case or null if not found.
     */
    recommendationForUseCase(useCase) {
        return this.recommendations.find((recommendation) => recommendation.useCase === useCase);
    }
}
/**
 * Today recommendation with a use case, candidates, metrics and a data container.
 */
export class TodayRecommendation {
    /**
     * Initializes today recommendation.
     * @param useCase - Use case that is used to match recommendations.
     * @param candidates - Recommended candidates to use as replacements.
     * @param recoMetrics - Metrics for given candidates.
     * @param dataContainer - Data returned from MAPI for recommended candidates.
     */
    constructor(useCase, candidates, recoMetrics, dataContainer) {
        this.useCase = useCase;
        this.candidates = candidates;
        this.recoMetrics = recoMetrics;
        this.dataContainer = dataContainer;
    }
    /// Returns the first candidate that has the given type.
    candidate(type) {
        return this.candidates.find((candidate) => candidate.type === type);
    }
}
/**
 * Today recommended candidate with its id, type, data
 * and candidate IDs (only if it is a candidate for a story group).
 */
export class TodayRecommendedCandidate {
    /**
     * Initializes today recommended candidate.
     * @param id - Candidate id i.e. story ID or story group ID.
     * @param type - Candidate type i.e. "editorial-items" or "editorial-item-groups".
     * @param candidateIDs - Candidate IDs.
     * @param data - Data to use while fetching from MAPI.
     */
    constructor(id, type, candidateIDs, data) {
        this.id = id;
        this.type = type;
        this.candidateIDs = candidateIDs;
        this.data = data;
    }
}
//# sourceMappingURL=on-device-recommendations-today.js.map