import { isNothing, isSome } from "@jet/environment/types/optional";
import { LintedMetricsEvent } from "../../api/models/metrics/metrics";
import * as serverData from "../../foundation/json-parsing/server-data";
import { BuyParameters } from "../../foundation/metrics/buy-parameters";
import { cookiesOf } from "../../foundation/metrics/cookies";
import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache";
import { URL } from "../../foundation/network/urls";
import { reduceSignificantDigits } from "../../foundation/util/math-util";
import * as constants from "./helpers/constants";
import { stripUniqueImpressionIdsIfNecessary } from "./helpers/impressions";
import { iAdDismissAdActionMetricsParameterStringToken, iAdURLLineItemParameterStringToken, iAdURLParameterStringToken, } from "./helpers/models";
import * as searchResultImpressions from "./helpers/search-result-impressions";
import * as searchFocusImpressions from "./helpers/search-focus-impressions";
import * as util from "./helpers/util";
import { MetricsReferralContext } from "./metrics-referral-context";
/**
 * A type which applies App Store business rules to metrics fields
 * and generates events which are ready for posting to figaro.
 */
export class EventLinter {
    /**
     * Create an event linter.
     *
     * @param options The options which specify various behaviors of the new linter.
     * This object will be frozen.
     */
    constructor(options) {
        this._options = Object.freeze(options);
    }
    // endsection
    // section Public Properties
    /**
     * Topic to use if an event fields blob does not specify one.
     */
    get defaultTopic() {
        return this._options.defaultTopic;
    }
    // endsection
    // section Utilities
    /**
     * Reduce the accuracy of fields in a blob according to
     * a given array of rules found in a metrics objectGraph.bag.
     *
     * @param eventFields The fields of an event to reduce the accuracy of.
     * @param rules An array of rules from a metrics objectGraph.bag.
     */
    _reduceFieldAccuracy(eventFields, rules) {
        for (const rule of rules) {
            const fieldName = serverData.asString(rule, "fieldName");
            if (serverData.isNull(fieldName)) {
                continue;
            }
            const value = serverData.asNumber(eventFields, fieldName);
            if (serverData.isNull(value)) {
                continue;
            }
            let magnitude = serverData.asNumber(rule, "magnitude");
            if (serverData.isNull(magnitude)) {
                magnitude = 1024 * 1024;
            }
            let significantDigits = serverData.asNumber(rule, "significantDigits");
            if (serverData.isNull(significantDigits)) {
                significantDigits = 2;
            }
            if (magnitude <= 0.0 || significantDigits < 0.0) {
                // This is the failure mode from MetricsKit.
                eventFields[fieldName] = Number.NaN;
                continue;
            }
            const scaledValue = value / magnitude;
            eventFields[fieldName] = reduceSignificantDigits(scaledValue, significantDigits);
        }
    }
    /**
     * Returns a new URL by scrubbing any ad fields we've inserted into URLs to pass
     * ad attribution information between pages.
     * @param urlString The original URL to be scrubbed
     * @returns A scrubbed URL.
     */
    _urlScrubbingAdParameters(urlString) {
        const url = new URL(urlString);
        url.removeParam(iAdURLParameterStringToken);
        url.removeParam(iAdURLLineItemParameterStringToken);
        url.removeParam(iAdDismissAdActionMetricsParameterStringToken);
        return url.build();
    }
    /**
     * Returns a new URL by scrubbing everything but the protocol, domain, and port (e.g. host).
     * @param urlString The original URL to be scrubbed
     * @returns A scrubbed URL.
     */
    _urlScrubbingExtRefUrl(urlString) {
        const url = new URL(urlString);
        url.username = "";
        url.password = "";
        url.pathname = undefined;
        url.query = undefined;
        url.hash = undefined;
        return url.build();
    }
    // endsection
    // section Fast Impressions Deresolution
    _derezFastImpressions(eventFields) {
        const impressionQueue = serverData.asString(eventFields, "impressionQueue");
        const eventVersion = serverData.asNumber(eventFields, "eventVersion");
        if (impressionQueue !== "data-metrics-impressions-low-latency") {
            return; // Only scrub data going to the AP low latency queue.
        }
        // Scrub `viewedInfo` of a v4 event.
        if (eventVersion === 4) {
            const impressions = serverData.asArrayOrEmpty(eventFields, "impressions");
            eventFields["impressions"] = impressions.map((impression) => {
                if (isNothing(impression)) {
                    return impression;
                }
                const viewedInfo = serverData.asArrayOrEmpty(impression, "viewedInfo");
                if (viewedInfo.length === 0) {
                    return impression; // V3. No modification.
                }
                /**
                 * <rdar://problem/64497066> Metrics: iAd: JS must clear impression start times, and derezz duration to 2 sig figs
                 * - start time to 0 (strange ask - we already send this as part of `impressionTimes`)
                 * - duration to 2 sig fig.
                 */
                impression["viewedInfo"] = viewedInfo.map((interval) => {
                    if (isNothing(interval)) {
                        return interval;
                    }
                    const duration = serverData.asNumber(interval, "d");
                    interval["s"] = 0;
                    if (isSome(duration)) {
                        interval["d"] = reduceSignificantDigits(duration, 2);
                    }
                    return interval;
                });
                return impression;
            });
        }
        // Scrub `viewedInfoDetailed` of a v5 event.
        if (eventVersion === 5) {
            const impressions = serverData.asArrayOrEmpty(eventFields, "impressions");
            eventFields["impressions"] = impressions.map((impression) => {
                if (isNothing(impression)) {
                    return impression;
                }
                // Impression items in a low latency impressions event shouldn't have a `cardType` field.
                delete impression["cardType"];
                if (serverData.isNullOrEmpty(serverData.asString(impression, "iAdMetadata")) ||
                    serverData.isNullOrEmpty(serverData.asString(impression, "iAdImpressionId"))) {
                    // If the iAdMetadata or iAdImpressionId for this impression on the fast queue is null,
                    // it's an organic result that has added instrumentation as it's in a defined ad slot.
                    // For these items, we have to scrub the adamId for privacy reasons.
                    delete impression["id"];
                }
                // Get the dictionary of values, formatted like so:
                // {0: [{s: 11636658562756, d: 23281}], 50: [{s: 1636658559350, d: 378}]}
                const viewedInfoDetailed = serverData.asDictionary(impression, "viewedInfoDetailed");
                if (isNothing(viewedInfoDetailed) || serverData.isNullOrEmpty(viewedInfoDetailed)) {
                    return impression; // Nothing to modify.
                }
                /**
                 * rdar://89785026 (Chainlink: v5 Impressions ViewedInfoDetailed - Fixes for startTime and duration)
                 * - match scrubbing "s" value and de-rezing "d" value as per above v4 implementation.
                 */
                Object.entries(viewedInfoDetailed).forEach(([key, value]) => {
                    // For each key/value pair, grab the array of traditional "viewedInfo".
                    const viewedInfo = serverData.asArrayOrEmpty(value);
                    // Iterate over the array of values, scrubbing the required children.
                    viewedInfoDetailed[key] = viewedInfo.map((interval) => {
                        if (isNothing(interval)) {
                            return interval;
                        }
                        const duration = serverData.asNumber(interval, "d");
                        interval["s"] = 0;
                        if (isSome(duration)) {
                            interval["d"] = reduceSignificantDigits(duration, 2);
                        }
                        return interval;
                    });
                });
                // Re-set the modified dictionary on the `impression` object.
                impression["viewedInfoDetailed"] = viewedInfoDetailed;
                return impression;
            });
        }
    }
    _stripContentRatingImpressionFields(eventFields) {
        const impressions = serverData.asArrayOrEmpty(eventFields, "impressions");
        eventFields["impressions"] = impressions.map((impression) => {
            if (isSome(impression)) {
                // "contentRating" and "bundleId" are used by native to
                // determine the value of "contentRestrictionReasons" (whether a
                // lockup's offer is disabled due to content restrictions).
                // However we don't want these fields in the final impression.
                delete impression["contentRating"];
                delete impression["bundleId"];
            }
            return impression;
        });
    }
    // endsection
    // section Search Results Page Impressions
    /**
     * Decorate `impressions` field for click and impressions events on SRP.
     */
    _decorateSearchResultImpressions(eventFields) {
        const pageType = serverData.asString(eventFields, "pageType");
        const pageId = serverData.asString(eventFields, "pageId");
        /**
         * Only run on SRP.
         */
        const isSearchResultsPage = pageType === "Search" && pageId !== "hints";
        if (isSearchResultsPage) {
            searchResultImpressions.decorateImpressionParentId(eventFields);
        }
    }
    // endsection
    // section Search Focus Page Impressions
    /**
     * Decorate `impressions` field for click and impressions events on SFP.
     */
    _decorateSearchFocusImpressions(eventFields) {
        const pageType = serverData.asString(eventFields, "pageType");
        const pageId = serverData.asString(eventFields, "pageId");
        /**
         * Only run on SFP.
         */
        if (pageType === "SearchFocus" && pageId === "Focus") {
            searchFocusImpressions.decorateImpressionParentId(eventFields);
        }
    }
    // endsection
    // section Rules
    /**
     * Apply the rules which are universal to all metrics events
     * to a given metrics fields linter.
     *
     * @param eventFields The fields which will be used to construct a built event.
     * @param topic The topic the built event will be submitted to.
     */
    _decorateAll(objectGraph, eventFields, topic) {
        var _a, _b, _c;
        const getBagValue = this._options.bagProvider;
        // - Metrics base fields
        const metricsBase = getBagValue("metricsBase", topic);
        if (!serverData.isNull(metricsBase) && typeof metricsBase === "object") {
            Object.assign(eventFields, metricsBase);
        }
        // - Universal basic fields
        eventFields["clientBuildType"] = this._options.buildType;
        eventFields["resourceRevNum"] = this._options.jsVersion;
        eventFields["xpSendMethod"] = "jet-js";
        this._options.buyDecorator.useApp(serverData.asString(eventFields, "app"));
        // - Universal scrubbing
        delete eventFields[constants.contextualAdamIdKey];
        // - Cookie-based fields
        const cookies = cookiesOf(serverData.asString(eventFields, "cookie"));
        for (const cookie of cookies) {
            if (cookie.key === "xp_ci") {
                // Always update buy decorator.
                this._options.buyDecorator.useClientId(cookie.value);
                break;
            }
        }
        delete eventFields["cookie"];
        const clientIdFields = (_b = (_a = objectGraph.metricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsFieldsForTypes([MetricsIdentifierType.client])) !== null && _b !== void 0 ? _b : {};
        Object.assign(eventFields, clientIdFields);
        delete eventFields["clientGeneratedId"];
        // - page
        const pageType = serverData.asString(eventFields, "pageType");
        const pageId = serverData.asString(eventFields, "pageId");
        if (!serverData.isNull(pageType) && !serverData.isNull(pageId)) {
            const separator = serverData.asString(getBagValue("compoundSeparator", topic)) || "_";
            eventFields["page"] = `${pageType}${separator}${pageId}`;
        }
        // - Field value resolution reduction
        const rules = serverData.asArrayOrEmpty(getBagValue("deResFields", topic));
        this._reduceFieldAccuracy(eventFields, rules);
        // Scrub sensitive urls in event from data that should not leave the device.
        // At times we insert ad metadata into URLs in order to pass ad attribution information between pages.
        // Additionally, scrub extRefUrl of everything but the origin.
        const urlFieldsToScrub = ["pageUrl", "actionUrl", "extRefUrl", "refUrl", "url", "parentPageUrl"];
        for (const urlField of urlFieldsToScrub) {
            const urlString = serverData.asString(eventFields, urlField);
            if (isSome(urlString) && urlString.length > 0) {
                eventFields[urlField] =
                    urlField === "extRefUrl"
                        ? this._urlScrubbingExtRefUrl(urlString)
                        : this._urlScrubbingAdParameters(urlString);
            }
        }
        // Where an `overridePageContext` has been provided, use it as the `pageContext` value.
        const overridePageContext = serverData.asString(eventFields, "overridePageContext");
        if (isSome(overridePageContext)) {
            delete eventFields["overridePageContext"];
            eventFields["pageContext"] = overridePageContext;
        }
        if (objectGraph.bag.isMetricsUserIdFallbackEnabled) {
            const existingUserId = serverData.asString(eventFields, "userId");
            let metricsUserDSID = null;
            if (isNothing(existingUserId) ||
                existingUserId.length === 0 ||
                existingUserId.length === EventLinter.clientGeneratedUserIdLength) {
                metricsUserDSID = (_c = objectGraph.user.dsid) !== null && _c !== void 0 ? _c : null;
            }
            if (isSome(metricsUserDSID) && metricsUserDSID.length > 0) {
                eventFields["dsId"] = metricsUserDSID;
            }
        }
    }
    _decorateClick(eventFields) {
        util.adjustGhostHintFieldsForClick(eventFields);
        // clicks have snapshot impressions field.
        this._decorateSearchResultImpressions(eventFields);
        this._decorateSearchFocusImpressions(eventFields);
        MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields);
        this._filterBuyParams(eventFields);
        /// rdar://118948967 (Disable snapshot impressions on iOS 17)
        const pageType = serverData.asString(eventFields, "pageType");
        if (isNothing(pageType) || !pageType.toLowerCase().includes("search")) {
            delete eventFields["impressions"];
        }
        stripUniqueImpressionIdsIfNecessary(eventFields);
    }
    /**
     * Apply the rules specific to the `impression` event.
     *
     * @param objectGraph Current object graph
     * @param eventFields The fields which will be used to construct a built event.
     * @returns Whether or not the impressions event is valid and non-empty
     */
    _decorateImpressions(objectGraph, eventFields) {
        if (serverData.isNullOrEmpty(eventFields["impressions"])) {
            return false;
        }
        this._derezFastImpressions(eventFields);
        this._decorateSearchResultImpressions(eventFields);
        this._stripContentRatingImpressionFields(eventFields);
        const refUrl = serverData.asString(eventFields, "refUrl");
        if (isSome(refUrl) && refUrl.length > 0) {
            eventFields["searchTerm"] = util.searchTermFromRefURL(refUrl);
            delete eventFields["refUrl"];
        }
        if (objectGraph.client.isVision) {
            const pageUrl = serverData.asString(eventFields, "pageUrl");
            if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) {
                const searchTerm = util.searchTermFromProductURL(pageUrl);
                if (isSome(searchTerm)) {
                    eventFields["searchTerm"] = searchTerm;
                }
            }
        }
        stripUniqueImpressionIdsIfNecessary(eventFields);
        // We need the impressionQueue for _derezFastImpressions, so we know which events to derez, but then we need to
        // remove it from the event
        // rdar://144333694 ([Ad Platforms][CrystalE][iOS]Seeing 'Impression Queue' as a top level
        // field for impressions which was removed earlier)
        delete eventFields["impressionQueue"];
        return true;
    }
    /**
     * Apply the rules specific to the `media` event.
     *
     * @param eventFields The fields which will be used to construct a built event.
     */
    _decorateMedia(eventFields) {
        const position = serverData.asNumber(eventFields, "position");
        if (!serverData.isNull(position)) {
            eventFields["position"] = Math.round(position);
        }
    }
    /**
     * Apply the rules specific to the `buyparams` .
     *
     * @param eventFields The fields which will be used to construct a built event.
     */
    _filterBuyParams(eventFields) {
        const buyParamsString = serverData.asString(eventFields, "actionDetails.buyParams");
        if (isSome(buyParamsString) && buyParamsString.length > 0) {
            const buyParams = new BuyParameters(buyParamsString);
            const disallowedFields = ["ownerDsid"];
            disallowedFields.forEach((key) => {
                buyParams.set(key, null, null);
            });
            if (isSome(eventFields["actionDetails"])) {
                eventFields["actionDetails"]["buyParams"] = buyParams.toString();
            }
        }
    }
    /**
     * Apply the rules specific to the `page` event.
     *
     * @param objectGraph Current object graph
     * @param eventFields The fields which will be used to construct a built event.
     */
    _decoratePage(objectGraph, eventFields) {
        const page = serverData.asString(eventFields, "page");
        if (!serverData.isNull(page)) {
            eventFields["pageHistory"] = this._options.buyDecorator.getPageHistoryFor(page);
        }
        MetricsReferralContext.shared.setReferralDataForProductPageExtensionIfNecessary(eventFields);
        MetricsReferralContext.shared.beginReferralContextForPageIfNecessary(eventFields);
        MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields);
        // Make sure to add the referral data before checking for valid refUrl
        const refUrl = serverData.asString(eventFields, "refUrl");
        if (!serverData.isNull(refUrl)) {
            const refApp = util.extractSiriRefAppFromRefURL(refUrl);
            const searchTerm = util.searchTermFromRefURL(refUrl);
            if (refApp !== null && refApp.length > 0) {
                eventFields["refApp"] = refApp;
            }
            if (searchTerm !== null && searchTerm.length > 0) {
                eventFields["searchTerm"] = searchTerm;
            }
        }
        if (objectGraph.client.isVision) {
            const pageUrl = serverData.asString(eventFields, "pageUrl");
            if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) {
                const searchTerm = util.searchTermFromProductURL(pageUrl);
                if (isSome(searchTerm)) {
                    eventFields["searchTerm"] = searchTerm;
                }
            }
        }
    }
    /**
     * Apply the rules specific to the `pageChange` event. The pageChange should have the same treatment
     * as page event.
     *
     * @param eventFields The fields which will be used to construct a built event.
     */
    _decoratePageChange(objectGraph, eventFields) {
        this._decoratePage(objectGraph, eventFields);
    }
    /**
     * Apply the rules specific to the `search` event.
     *
     * @param objectGraph Current object graph
     * @param eventFields The fields which will be used to construct a built event.
     */
    _decorateSearch(eventFields) {
        eventFields["eventVersion"] = 3;
        util.adjustGhostHintFieldsForSearch(eventFields);
    }
    /**
     * Apply the rules specific to the `pageExit` event.
     *
     * @param eventFields The fields which will be used to construct a built event.
     */
    _decoratePageExit(eventFields) {
        MetricsReferralContext.shared.endReferralContextIfNecessaryForPageEvent(eventFields);
    }
    // endsection
    // section Filter
    _filterExtraneous(eventFields) {
        util.removeExtraGhostHintFields(eventFields);
        MetricsReferralContext.shared.removeReferralContextInfoFromMetricsEvent(eventFields);
    }
    // endsection
    // section Building Events
    /**
     * Create a metrics event by applying the business rules of App Store to a given fields blob.
     *
     * @param eventFields The fields to use to construct an event.
     * @returns A built event ready for posting.
     */
    makeEvent(objectGraph, eventFields) {
        var _a, _b;
        if (preprocessor.GAMES_TARGET) {
            // Until we get further with Privacy/Legal, the decision has been made to disable instrumentation
            // for Game Overlay. the pre-consent fields provider is only active for Game Overlay. Other options
            // for more fine-tuned control (_gameCenterPreConsent and _crossUsePreConsent) have been added to
            // provide JS flexibility.
            const isPreConsentFieldsProviderEnabled = eventFields["_isPreConsentFieldsProviderEnabled"];
            const gameCenterPreConsent = eventFields["_gameCenterPreConsent"];
            const crossUsePreConsent = eventFields["_crossUsePreConsent"];
            if (isPreConsentFieldsProviderEnabled === true ||
                gameCenterPreConsent === true ||
                crossUsePreConsent === true) {
                return new LintedMetricsEvent({});
            }
            delete eventFields["_isPreConsentFieldsProviderEnabled"];
            delete eventFields["_gameCenterPreConsent"];
            delete eventFields["_crossUsePreConsent"];
        }
        const eventType = serverData.asString(eventFields, "eventType");
        if (this._options.isLoggingEnabled) {
            objectGraph.console.log(`Building event for topic: ${eventType}`);
        }
        // rdar://135738684 (TLF: Off Store Lockups Removal)
        // Currently all events from App Store Components are suppressed.
        // Delete all fields to leave an empty event.
        const app = eventFields["app"];
        if (app === "com.apple.appstorecomponentsd") {
            return new LintedMetricsEvent({});
        }
        const topic = serverData.asString(eventFields, "topic") || this._options.defaultTopic;
        this._decorateAll(objectGraph, eventFields, topic);
        // rdar://148554411 (Metrics: Disable PII collection from U13 accounts)
        let isUnderThirteenAccount;
        if (preprocessor.GAMES_TARGET && objectGraph.props.enabled("157263806-add-playerBridge-askGlobal")) {
            isUnderThirteenAccount = (_b = (_a = objectGraph.player) === null || _a === void 0 ? void 0 : _a.isUnderThirteen) !== null && _b !== void 0 ? _b : false;
        }
        else {
            isUnderThirteenAccount = objectGraph.user.isUnderThirteen;
        }
        if (isUnderThirteenAccount) {
            delete eventFields["dsId"];
            delete eventFields["userId"];
            delete eventFields["canonicalAccountIdentifierOverride"];
        }
        const extRefUrl = eventFields["extRefUrl"];
        if (extRefUrl && extRefUrl === "") {
            delete eventFields["extRefUrl"];
        }
        switch (eventType) {
            case "click":
                this._decorateClick(eventFields);
                break;
            case "exit":
                break;
            case "impressions":
                const isValidEvent = this._decorateImpressions(objectGraph, eventFields);
                if (!isValidEvent) {
                    // We want to filter out empty impressions events,
                    // and passing an empty event will contractually drop it from the metrics recorder
                    return new LintedMetricsEvent({});
                }
                break;
            case "media":
                this._decorateMedia(eventFields);
                break;
            case "page":
                this._decoratePage(objectGraph, eventFields);
                break;
            case "pageChange":
                this._decoratePageChange(objectGraph, eventFields);
                break;
            case "pageExit":
                this._decoratePageExit(eventFields);
                break;
            case "search":
                this._decorateSearch(eventFields);
                break;
            default:
                break;
        }
        this._filterExtraneous(eventFields);
        if (objectGraph.bag.metricsIdMigrationEnabled) {
            util.removeDSIDFields(eventFields);
        }
        return new LintedMetricsEvent(eventFields);
    }
}
/**
 * The length of a client generated user ID.
 */
EventLinter.clientGeneratedUserIdLength = 24;
/**
 * Key whose value indicates if event fields contain iAd data.
 */
EventLinter.hasIAdData = "hasiAdData";
//# sourceMappingURL=event-linter.js.map