import { isNothing, isSome } from "@jet/environment/types/optional";
import * as serverData from "../json-parsing/server-data";
import * as client from "../wrappers/client";
export function allPlatforms(objectGraph, platformsToExclude) {
    const platforms = new Set();
    platforms.add("iphone");
    platforms.add("ipad");
    platforms.add("appletv");
    platforms.add("mac");
    platforms.add("watch");
    if (objectGraph.client.isVision || objectGraph.bag.enableVisionPlatform) {
        platforms.add("realityDevice");
    }
    if (isSome(platformsToExclude)) {
        for (const platform of platformsToExclude) {
            platforms.delete(platform);
        }
    }
    return Array.from(platforms);
}
export function defaultPlatformForClient(objectGraph) {
    if (objectGraph.client.isCompanionVisionApp) {
        // The Vision companion app always prefers visionOS assets
        return "realityDevice";
    }
    switch (objectGraph.client.deviceType) {
        case "phone":
            return "iphone";
        case "pad":
            return "ipad";
        case "tv":
            return "appletv";
        case "mac":
            return "mac";
        case "watch":
            return "watch";
        case "vision":
            return "realityDevice";
        case "web":
            return "web";
        default:
            return null;
    }
}
/**
 * Returns the layout size controlling the number of rows to display.
 * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type.
 * @returns {number} Layout size.
 */
export function defaultLayoutSize(objectGraph) {
    return objectGraph.client.isPhone ? 2 : 1;
}
/**
 * Returns the sparse count controlling the number of shelves to hydrate.
 * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type.
 * @returns {number} Sparse count.
 */
export function defaultSparseCountForClient(objectGraph) {
    switch (objectGraph.client.deviceType) {
        case "phone":
            return 4;
        case "pad":
            return 5;
        case "tv":
            return 6;
        case "mac":
            return 5;
        case "watch":
            return 10;
        default:
            return null;
    }
}
/**
 * Returns the sparse limit controlling the number of items to hydrate per shelve.
 * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type.
 * @returns {number} Sparse limit.
 */
export function defaultSparseLimitForClient(objectGraph) {
    switch (objectGraph.client.deviceType) {
        case "phone":
            return 9;
        case "pad":
            return 12;
        case "tv":
            return 15;
        case "mac":
            return 15;
        case "watch":
            return 3;
        case "web":
            return 12;
        default:
            return null;
    }
}
/**
 * Get the list of all platforms excluding the current platform
 * @returns {Platform[]}
 */
export function defaultAdditionalPlatformsForClient(objectGraph) {
    switch (objectGraph.host.clientIdentifier) {
        case "com.apple.TVAppStore.AppStoreTopShelfExtension":
        case "com.apple.Arcade.ArcadeTopShelfExtension":
        case "com.apple.AppStore.Widgets":
            // Skip additional platforms for top shelf extension due to memory constraint.
            return [];
        default: {
            const currentPlatform = defaultPlatformForClient(objectGraph);
            return allPlatforms(objectGraph, isSome(currentPlatform) ? new Set([currentPlatform]) : undefined);
        }
    }
}
/**
 * Get the product page reviews limit for a client.
 * @returns {number} The limit to use.
 */
export function defaultProductPageReviewsLimitForClient(objectGraph) {
    switch (objectGraph.client.deviceType) {
        case "phone":
            return 6;
        case "pad":
            return 10;
        case "mac":
            return 12;
        case "vision":
            return 10;
        default:
            return 8;
    }
}
/// Returns the context param to use for a given grouping request.
/// This is leveraged for switching grouping tab root in specific configurations.
export function defaultGroupingContextForClient(objectGraph) {
    let context = null;
    if (objectGraph.host.clientIdentifier === client.messagesIdentifier) {
        // Messages Grouping (iOS)
        context = "messages";
    }
    else if (objectGraph.host.clientIdentifier === client.watchIdentifier) {
        // Bridge App Store (iOS)
        context = "watch";
    }
    else if (objectGraph.client.isWatch && objectGraph.client.isTinkerWatch) {
        // Tinker App Store (watchOS)
        context = "tinker";
    }
    return context;
}
// Nothing on the App Store is rated 1000+, so a value this high essentially
// indicates that content restrictions are disabled.
export const ageRestrictionsDisabledThreshold = 1000;
export class Request {
    constructor(objectGraph, param, enableMixedCatalog, supplementaryMetadataAssociations) {
        var _a;
        /// The resource types used in a mixed contents call
        this.contentsResourceTypes = new Set();
        /// The ID(s) associated with the resource type
        this.ids = new Set();
        /// IDs associated with specific resource types, used for mixed catalog requests
        this.idsByResourceType = new Map();
        /// The original ordering of the ids in this reqeust if there is a mixed catalog request
        this.originalOrdering = [];
        this.relationshipIncludes = new Set();
        // Contents of `extend` param
        this.attributeIncludes = new Set();
        this.platform = null;
        /// The paths to additional metadata that need to be fetched for mixed media requests
        this.supplementaryMetadataAssociations = [];
        this.additionalPlatforms = new Set();
        this.additionalQuery = {};
        this.relationshipLimits = {};
        this.searchTerm = null;
        this.searchTypes = [];
        /// Whether we are searching watch or messages store.
        this.context = null;
        /// Whether or not to use custom extend attributes, instead of using `attributeIncludes` as-is.
        this.useCustomAttributes = false;
        /// An optional country code override for constructing the URL. Used in very specific regulatory scenarios where the bag country code cannot be relied upon.
        this.countryCodeOverride = undefined;
        this.objectGraph = objectGraph;
        this.platform = defaultPlatformForClient(objectGraph);
        this.isMixedMediaRequest = enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false;
        this.supplementaryMetadataAssociations = supplementaryMetadataAssociations !== null && supplementaryMetadataAssociations !== void 0 ? supplementaryMetadataAssociations : [];
        this.includeAppBinaryTraitsAttribute = objectGraph.client.isiOS;
        // By default, the `platform` for web defaults to whatever the `activeIntent` is (e.g. "mac"),
        // but this can be overridden when calling `setPreviewPlatform` with the request, which
        // will set the `platform` to `web` and `previewPlatform` will be the `activeIntent` platform.
        if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) {
            this.platform = objectGraph.activeIntent.platform;
        }
        if (serverData.isNullOrEmpty(param)) {
            return;
        }
        if (typeof param === "string") {
            this.href = param;
        }
        else if (Array.isArray(param)) {
            this.withDataItems(param, supplementaryMetadataAssociations, enableMixedCatalog);
        }
    }
    /**
     * Adds a data object to an idsByResourceType mapping.
     * @param data The data object
     * @returns An updated {resource-type: IDs} map, which includes the data object
     */
    addDataToIDsByResourceType(data) {
        const resourceType = data.type;
        const id = data.id;
        let ids = this.idsByResourceType.get(resourceType);
        if (isNothing(ids)) {
            ids = new Set();
        }
        ids.add(id);
        this.idsByResourceType.set(resourceType, ids);
    }
    forType(type) {
        this.resourceType = type;
        return this;
    }
    /**
     * This method is used to add data items to the request. It is used to build the request for the mixed catalog / catalog
     *
     * @param dataItems The data items to add to the request
     * @param supplementaryMetadataAssociations The metadata paths to add to the request
     * @param enableMixedCatalog Whether to allow mixed catalog requests
     * @returns The modified Request
     */
    withDataItems(dataItems, supplementaryMetadataAssociations, enableMixedCatalog) {
        if (dataItems.length === 0) {
            return this;
        }
        this.isMixedMediaRequest = this.isMixedMediaRequest || (enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false);
        for (const data of dataItems) {
            // Track all IDs in a single set, for the case where we use the contents endpoint.
            this.ids.add(data.id);
            // Track all IDs by resource type, for either the case where we ultimately have just
            // one resource type, or where we used the mixed catalog endpoint.
            this.addDataToIDsByResourceType(data);
            // If we have any additional metadata paths that need fetching, we now add them
            // as additional resource types for the mixed catalog endpoint.
            if (isSome(enableMixedCatalog) &&
                enableMixedCatalog &&
                isSome(supplementaryMetadataAssociations) &&
                supplementaryMetadataAssociations.length > 0) {
                for (const association of supplementaryMetadataAssociations) {
                    const metadataItems = extractMetaAssociationFromData(association, data);
                    if (isSome(metadataItems) && metadataItems.length > 0) {
                        metadataItems.forEach((metadataItem) => {
                            this.addDataToIDsByResourceType(metadataItem);
                        });
                    }
                }
            }
        }
        // Now that we have collated all IDs and resource types, we can assign them to different
        // properties depending on the number of resource types we need.
        if (this.idsByResourceType.size === 1) {
            // We only have one list of IDs for a single resource type, so we just take the first one
            this.resourceType = this.idsByResourceType.keys().next().value;
            this.isMixedMediaRequest = false;
        }
        else if (this.idsByResourceType.size > 1 && !this.isMixedMediaRequest) {
            this.resourceType = "contents";
            this.contentsResourceTypes = new Set(Array.from(this.idsByResourceType.keys()));
        }
        this.originalOrdering.push(...[...dataItems]);
        return this;
    }
    /**
     * Add a single id to this request, this should only be used when fetching a single resource
     * @param id The single ID to add to the request
     * @param type The type of the resource
     * @returns The modified Request
     */
    withIdOfType(id, type) {
        return this.withDataItems([{ id, type }]);
    }
    /**
     * Add a list of ids to this request, this will add these ids to the mapping of resource types
     * @param ids The IDs to add to the request
     * @param type The type of the resource
     * @returns The modified Request
     */
    withIdsOfType(ids, type) {
        return this.withDataItems(ids.map((id) => ({ id, type })));
    }
    includingRelationships(relationships) {
        for (const relationship of relationships) {
            this.relationshipIncludes.add(relationship);
        }
        return this;
    }
    includingScopedRelationships(scopedDataType, relationshipsToAdd) {
        // Lazy init top-level property
        if (!this.scopedRelationshipIncludes) {
            this.scopedRelationshipIncludes = new Map();
        }
        // Retrieve existing scoped relationship. Lazy init if needed.
        let scopedRelationship = this.scopedRelationshipIncludes.get(scopedDataType);
        if (!scopedRelationship) {
            scopedRelationship = new Set();
        }
        // Update
        for (const newRelationship of relationshipsToAdd) {
            scopedRelationship.add(newRelationship);
        }
        this.scopedRelationshipIncludes.set(scopedDataType, scopedRelationship);
        return this;
    }
    includingMetaKeys(scopedDataType, keysToAdd) {
        // Lazy init top-level property
        if (!this.metaIncludes) {
            this.metaIncludes = new Map();
        }
        // Retrieve existing inclusion. Lazy init if needed.
        let scopedMeta = this.metaIncludes.get(scopedDataType);
        if (!scopedMeta) {
            scopedMeta = new Set();
        }
        // Update
        for (const newKey of keysToAdd) {
            scopedMeta.add(newKey);
        }
        this.metaIncludes.set(scopedDataType, scopedMeta);
        return this;
    }
    includingViews(viewsToAdd) {
        // Lazy init top-level property
        if (!this.viewsIncludes) {
            this.viewsIncludes = new Set();
        }
        for (const view of viewsToAdd) {
            this.viewsIncludes.add(view);
        }
        return this;
    }
    includingKindsKeys(scopedDataType, keysToAdd) {
        // Lazy init top-level property
        if (!this.kindIncludes) {
            this.kindIncludes = new Map();
        }
        // Retrieve existing inclusion. Lazy init if needed.
        let scopedMeta = this.kindIncludes.get(scopedDataType);
        if (!scopedMeta) {
            scopedMeta = new Set();
        }
        // Update
        for (const newKey of keysToAdd) {
            scopedMeta.add(newKey);
        }
        this.kindIncludes.set(scopedDataType, scopedMeta);
        return this;
    }
    includingAssociateKeys(scopedDataType, keysToAdd) {
        // Lazy init top-level property
        if (!this.associateIncludes) {
            this.associateIncludes = new Map();
        }
        // Retrieve existing inclusion. Lazy init if needed.
        let scopedAssociate = this.associateIncludes.get(scopedDataType);
        if (!scopedAssociate) {
            scopedAssociate = new Set();
        }
        // Update
        for (const newKey of keysToAdd) {
            scopedAssociate.add(newKey);
        }
        this.associateIncludes.set(scopedDataType, scopedAssociate);
        return this;
    }
    /**
     * Include the relationships needed for an upsell.
     * @param requiresScopedInclude A flag indicating whether the include should be "scoped". This will need to be the
     * case when the relationships we are including is on a request for a separate primary resource type. For example,
     * if we are looking to have the upsell relationship included in an EI (Today card, for example), this needs to be
     * scoped. However, if we are fetching a grouping directly and need the upsell included, this should *not* be scoped.
     */
    includingRelationshipsForUpsell(requiresScopedRelationshipInclude) {
        const relationship = "marketing-items";
        if (requiresScopedRelationshipInclude) {
            // Lazy init top-level property
            if (!this.scopedRelationshipIncludes) {
                this.scopedRelationshipIncludes = new Map();
            }
            // Retrieve existing scoped relationship. Lazy init if needed.
            let scopedRelationship = this.scopedRelationshipIncludes.get("editorial-items");
            if (!scopedRelationship) {
                scopedRelationship = new Set();
            }
            // Update
            scopedRelationship.add(relationship);
            this.scopedRelationshipIncludes.set("editorial-items", scopedRelationship);
        }
        else {
            this.relationshipIncludes.add(relationship);
        }
        // In order to get metrics metadata stiched in to the marketing item relationship, we have to include it in our
        // request.
        if (relationship === "marketing-items") {
            if (!this.metaIncludes) {
                this.metaIncludes = new Map();
            }
            // Retrieve existing inclusion. Lazy init if needed.
            let scopedMeta = this.metaIncludes.get("marketing-items");
            if (!scopedMeta) {
                scopedMeta = new Set();
            }
            scopedMeta.add("metrics");
            this.metaIncludes.set("marketing-items", scopedMeta);
        }
        return this;
    }
    includingAttributes(attributes) {
        for (const attribute of attributes) {
            this.attributeIncludes.add(attribute);
        }
        return this;
    }
    includingScopedAttributes(resourceType, attributesToAdd) {
        // Lazy init top-level property
        if (!this.scopedAttributeIncludes) {
            this.scopedAttributeIncludes = new Map();
        }
        // Retrieve existing scoped relationship. Lazy init if needed.
        let attributesForResourceType = this.scopedAttributeIncludes.get(resourceType);
        if (!attributesForResourceType) {
            attributesForResourceType = new Set();
        }
        // Update
        for (const attribute of attributesToAdd) {
            attributesForResourceType.add(attribute);
        }
        this.scopedAttributeIncludes.set(resourceType, attributesForResourceType);
        return this;
    }
    /**
     * Adds an age restriction to the request. ManagedConfiguration provides
     * this value on the client, which maps to JRPurpleRating.clientIdentifier.
     *
     * @returns {Request} The updated request
     */
    includingAgeRestrictions() {
        const maxAppContentRating = this.objectGraph.client.maxAppContentRating;
        if (maxAppContentRating < ageRestrictionsDisabledThreshold) {
            this.ageRestriction = maxAppContentRating;
        }
        return this;
    }
    includingAdditionalPlatforms(additionalPlatforms) {
        for (const platform of additionalPlatforms) {
            this.additionalPlatforms.add(platform);
        }
        return this;
    }
    includingScopedAvailableIn(resourceType, valuesToAdd) {
        // Lazy init top-level property
        if (!this.scopedAvailableInIncludes) {
            this.scopedAvailableInIncludes = new Map();
        }
        // Retrieve existing scoped relationship. Lazy init if needed.
        let valuesForResourceType = this.scopedAvailableInIncludes.get(resourceType);
        if (!valuesForResourceType) {
            valuesForResourceType = new Set();
        }
        // Update
        for (const value of valuesToAdd) {
            valuesForResourceType.add(value);
        }
        this.scopedAvailableInIncludes.set(resourceType, valuesForResourceType);
        return this;
    }
    /**
     * Include the sparse limit needed for a specific resource type.
     * @param {media.Type} resourceType Resource type to use for the sparse limit.
     * @param {number} value Value to set as the sparse limit.
     * @returns {Request} Updated request.
     */
    includingScopedSparseLimit(resourceType, value) {
        // Lazy init top-level property
        if (!this.scopedSparseLimit) {
            this.scopedSparseLimit = new Map();
        }
        this.scopedSparseLimit.set(resourceType, value);
        return this;
    }
    addingQuery(key, value) {
        if (isSome(value)) {
            this.additionalQuery[key] = value;
        }
        else {
            delete this.addingQuery[key];
        }
        return this;
    }
    /**
     * @param query The additional query values to add
     * @returns The modified Reqeust
     */
    addingQueryValues(query) {
        this.additionalQuery = {
            ...this.addingQuery,
            ...query,
        };
        return this;
    }
    addingRelationshipLimit(relationship, limit) {
        this.relationshipLimits[relationship] = limit;
        return this;
    }
    withSearchTerm(term) {
        this.searchTerm = term;
        return this;
    }
    searchingOverTypes(types) {
        for (const type of types) {
            this.searchTypes.push(type);
        }
        return this;
    }
    addingContext(context) {
        this.context = context;
        return this;
    }
    includingMacOSCompatibleIOSAppsWhenSupported(verifiedBadgeOnly = false) {
        if (this.objectGraph.appleSilicon.isSupportEnabled) {
            if (!verifiedBadgeOnly) {
                this.enablingFeature("macOSCompatibleIOSApps");
            }
            this.includingScopedAttributes("apps", ["isVerifiedForAppleSiliconMac"]);
        }
        return this;
    }
    /// Mark a request to include (or exclude) appBinaryTraits attributes.
    includingAppBinaryTraitsAttribute(includeAppBinaryTraitsAttribute = true) {
        this.includeAppBinaryTraitsAttribute = includeAppBinaryTraitsAttribute;
        return this;
    }
    /**
     * Mark a request to use custom attributes.
     * This triggers usage of `customXYZ` extend attributes in url-builder for specific extend params
     * e.g. `artwork`, `customArtwork`.
     */
    usingCustomAttributes(useCustomAttributes) {
        this.useCustomAttributes = useCustomAttributes;
        return this;
    }
    alwaysUseIdsAsQueryParam(value) {
        this.useIdsAsQueryParam = value;
        return this;
    }
    attributingTo(canonicalUrl) {
        this.canonicalUrl = canonicalUrl;
        return this;
    }
    withFilter(type, value) {
        this.filterType = type;
        this.filterValue = value;
        return this;
    }
    withLimit(limit) {
        this.limit = limit;
        return this;
    }
    withSparseLimit(sparseLimit) {
        if (sparseLimit !== null) {
            this.sparseLimit = sparseLimit;
        }
        return this;
    }
    withSparseCount(sparseCount) {
        if (sparseCount !== null) {
            this.sparseCount = sparseCount;
        }
        return this;
    }
    enablingFeature(feature) {
        if (!this.enabledFeatures) {
            this.enabledFeatures = [];
        }
        this.enabledFeatures.push(feature);
        return this;
    }
    enablingFeatures(features) {
        if (!this.enabledFeatures) {
            this.enabledFeatures = [];
        }
        this.enabledFeatures.push(...features);
        return this;
    }
    /**
     * Limit the request to include only certain fields.
     * This should only be used in very select use-cases, where:
     * - There is no possibility of data being used for other purposes, e.g. sidpack
     * - All consumer of this data use it in the same narrow scope, e.g. requesting a set of icons for displaying without metadata only.
     * @param fields Set of fields to limit to
     */
    asPartialResponseLimitedToFields(fields) {
        this.fields = fields;
        return this;
    }
    includesResourceType(resourceType) {
        if (this.resourceType === resourceType) {
            return true;
        }
        if (serverData.isDefinedNonNull(this.contentsResourceTypes)) {
            return this.contentsResourceTypes.has(resourceType);
        }
        return false;
    }
    withCountryCodeOverride(countryCodeOverride) {
        this.countryCodeOverride = countryCodeOverride;
        return this;
    }
}
/**
 * Extracts the metadata association from the data.
 * @param association The association to extract
 * @param data The data to extract from
 */
export function extractMetaAssociationFromData(association, data) {
    if (serverData.isNullOrEmpty(data)) {
        return null;
    }
    const associationData = serverData.asArrayOrEmpty(data, `meta.associations.${association}.data`);
    if (isNothing(associationData)) {
        return null;
    }
    return [...associationData];
}
//# sourceMappingURL=data-fetching.js.map