/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// We use importESModule here instead of static import so that
// the Karma test environment won't choke on this module. This
// is because the Karma test environment already stubs out
// XPCOMUtils, and overrides importESModule to be a no-op (which
// can't be done for a static import statement).

// eslint-disable-next-line mozilla/use-static-import
const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

import {
  actionCreators as ac,
  actionTypes as at,
} from "resource://newtab/common/Actions.mjs";
import { Prefs } from "resource://newtab/lib/ActivityStreamPrefs.sys.mjs";
import {
  PREF_DEFAULT_VALUE_TOPSTORIES_ENABLED,
  PREF_DEFAULT_VALUE_TOPSITES_ENABLED,
} from "resource://newtab/lib/ActivityStream.sys.mjs";

// eslint-disable-next-line mozilla/use-static-import
const { AppConstants } = ChromeUtils.importESModule(
  "resource://gre/modules/AppConstants.sys.mjs"
);

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  Region: "resource://gre/modules/Region.sys.mjs",
  SelectableProfileService:
    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
});

const ACTIVATION_WINDOW_VARIANT_PREF = "activationWindow.variant";
const ACTIVATION_WINDOW_ENTER_MESSAGE_ID_PREF =
  "activationWindow.enterMessageID";
const ACTIVATION_WINDOW_EXIT_MESSAGE_ID_PREF = "activationWindow.exitMessageID";
const TOP_SITES_ENABLED_PREF = "feeds.topsites";
const TOP_STORIES_ENABLED_PREF = "feeds.section.topstories";
const TOP_SITES_USER_VALUE_TEMP_PREF =
  "activationWindow.temp.topSitesUserValue";
const TOP_STORIES_USER_VALUE_TEMP_PREF =
  "activationWindow.temp.topStoriesUserValue";
const PREF_DEFAULTS = [
  { type: "bool", key: "logowordmark.alwaysVisible", defaultValue: false },
  { type: "bool", key: "feeds.section.topstories", defaultValue: false },
  { type: "bool", key: "discoverystream.enabled", defaultValue: false },
  {
    type: "bool",
    key: "discoverystream.hardcoded-basic-layout",
    defaultValue: false,
  },
  { type: "string", key: "discoverystream.spocs-endpoint", defaultValue: "" },
  {
    type: "string",
    key: "discoverystream.spocs-endpoint-query",
    defaultValue: "",
  },
  {
    type: "string",
    key: "discoverystream.sections.personalization.inferred.debug.override",
    defaultValue: "",
  },
  { type: "string", key: "newNewtabExperience.colors", defaultValue: "" },
];

ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
  return console.createInstance({
    prefix: "PrefsFeed",
    maxLogLevel: Services.prefs.getBoolPref(
      "browser.newtabpage.activity-stream.activationWindow.log",
      false
    )
      ? "Debug"
      : "Warn",
  });
});

export class PrefsFeed {
  constructor(prefMap) {
    this._prefMap = prefMap;
    this._prefs = new Prefs();
    this.onExperimentUpdated = this.onExperimentUpdated.bind(this);
    this.onTrainhopExperimentUpdated =
      this.onTrainhopExperimentUpdated.bind(this);
    this.onPocketExperimentUpdated = this.onPocketExperimentUpdated.bind(this);
    this.onSmartShortcutsExperimentUpdated =
      this.onSmartShortcutsExperimentUpdated.bind(this);
    this.onWidgetsUpdated = this.onWidgetsUpdated.bind(this);
    this.onOhttpImagesUpdated = this.onOhttpImagesUpdated.bind(this);
    this.onInferredPersonalizationExperimentUpdated =
      this.onInferredPersonalizationExperimentUpdated.bind(this);
    this.onAdsBackendUpdated = this.onAdsBackendUpdated.bind(this);

    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "inActivationWindowState",
      this._prefs._branchStr + ACTIVATION_WINDOW_VARIANT_PREF,
      ""
    );
  }

  /**
   * Handles preference changes by broadcasting them to content processes and
   * tracking user changes during the activation window.
   *
   * @param {string} name - The preference name
   * @param {boolean | number | string} value - The new preference value
   * @param {boolean} [isUserChange=true] - Whether this change originated from
   *   a user action (true) or programmatic state transition (false). Only user
   *   changes are tracked during the activation window.
   */
  onPrefChanged(name, value, isUserChange = true) {
    const prefItem = this._prefMap.get(name);
    if (prefItem) {
      let action = "BroadcastToContent";
      if (prefItem.skipBroadcast) {
        action = "OnlyToMain";
        if (prefItem.alsoToPreloaded) {
          action = "AlsoToPreloaded";
        }
      }

      this.store.dispatch(
        ac[action]({
          type: at.PREF_CHANGED,
          data: { name, value },
        })
      );
    }

    if (isUserChange && this.inActivationWindowState) {
      this.trackActivationWindowPrefChange(name, value);
    }
  }

  /**
   * Tracks user preference changes during the activation window.
   *
   * @param {string} name - The preference name
   * @param {boolean | number | string} value - The new preference value
   */
  trackActivationWindowPrefChange(name, value) {
    if (name === TOP_SITES_ENABLED_PREF) {
      this._prefs.set(TOP_SITES_USER_VALUE_TEMP_PREF, value);
      lazy.logConsole.debug(
        `User set top sites to ${value} during activation window`
      );
    } else if (name === TOP_STORIES_ENABLED_PREF) {
      this._prefs.set(TOP_STORIES_USER_VALUE_TEMP_PREF, value);
      lazy.logConsole.debug(
        `User set top stories to ${value} during activation window`
      );
    }
  }

  _setStringPref(values, key, defaultValue) {
    this._setPref(values, key, defaultValue, Services.prefs.getStringPref);
  }

  _setBoolPref(values, key, defaultValue) {
    this._setPref(values, key, defaultValue, Services.prefs.getBoolPref);
  }

  _setIntPref(values, key, defaultValue) {
    this._setPref(values, key, defaultValue, Services.prefs.getIntPref);
  }

  _setPref(values, key, defaultValue, getPrefFunction) {
    let value = getPrefFunction(
      `browser.newtabpage.activity-stream.${key}`,
      defaultValue
    );
    values[key] = value;
    this._prefMap.set(key, { value });
  }

  /**
   * Handler for when experiment data updates.
   */
  onExperimentUpdated() {
    const value = lazy.NimbusFeatures.newtab.getAllVariables() || {};
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "featureConfig",
          value,
        },
      })
    );
  }

  /**
   * Computes the trainhop config by processing all enrollments.
   * Supports two formats:
   * - Single payload: { type: "feature", payload: { "enabled": true, ... }}
   * - Multi-payload: { type: "multi-payload", payload: [{ type: "feature", payload: { "enabled": true, ... }}] }
   * Both formats output the same structure: { "feature": { "enabled": true, ... }}
   */
  _getTrainhopConfig() {
    const allEnrollments =
      lazy.NimbusFeatures.newtabTrainhop.getAllEnrollments() || [];

    let enrollmentsToProcess = [];

    allEnrollments.forEach(enrollment => {
      if (
        enrollment?.value?.type === "multi-payload" &&
        Array.isArray(enrollment?.value?.payload)
      ) {
        enrollment.value.payload.forEach(item => {
          if (item?.type && item?.payload) {
            enrollmentsToProcess.push({
              value: {
                type: item.type,
                payload: item.payload,
              },
              meta: enrollment.meta,
            });
          }
        });
      } else if (enrollment?.value?.type) {
        enrollmentsToProcess.push(enrollment);
      }
    });

    const valueObj = {};
    enrollmentsToProcess.reduce((accumulator, currentValue) => {
      if (currentValue?.value?.type) {
        if (
          !accumulator[currentValue.value.type] ||
          (accumulator[currentValue.value.type].meta.isRollout &&
            !currentValue.meta.isRollout)
        ) {
          accumulator[currentValue.value.type] = currentValue;
          valueObj[currentValue.value.type] = currentValue.value.payload;
        }
      }
      return accumulator;
    }, {});

    // Bug 2021055: Write weather.display to the default branch so Nimbus sets
    // the initial value without overriding an explicit user choice (user branch
    // always takes precedence over the default branch).
    if (valueObj.widgets?.weatherForecastEnabled && valueObj.weather?.display) {
      Services.prefs
        .getDefaultBranch(this._prefs._branchStr)
        .setStringPref("weather.display", valueObj.weather.display);
    }

    // Write topSitesRows to the default branch to enable experiments with
    // the default row count without overriding an explicit user choice.
    if (valueObj.topSites?.topSitesRows) {
      Services.prefs
        .getDefaultBranch(this._prefs._branchStr)
        .setIntPref("topSitesRows", valueObj.topSites.topSitesRows);
    }

    return valueObj;
  }

  /**
   * Computes the adsBackend features by processing all enrollments.
   * This only looks at the flags variable within the adsBackend feature
   * and merges each flags object, preferring an experiment over a rollout.
   *
   */
  _getAdsBackendFeatures() {
    /**
     * @backward-compat { version 149 }
     *
     * We can replace `adsBackend?` with `adsBackend` once 149 hits the
     * release channel.
     */
    const allEnrollments =
      lazy.NimbusFeatures.adsBackend?.getAllEnrollments() || [];

    const valueObj = {};
    allEnrollments.reduce((accumulator, currentValue) => {
      if (currentValue?.value?.flags) {
        for (const [key, value] of Object.entries(currentValue.value.flags)) {
          if (
            !accumulator[key] ||
            (accumulator[key].meta.isRollout && !currentValue.meta.isRollout)
          ) {
            accumulator[key] = currentValue;
            valueObj[key] = value;
          }
        }
      }
      return accumulator;
    }, {});

    return valueObj;
  }

  /**
   * Handler for when experiment data updates.
   */
  onTrainhopExperimentUpdated() {
    const valueObj = this._getTrainhopConfig();

    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "trainhopConfig",
          value: valueObj,
        },
      })
    );
  }

  /**
   * Handler for Pocket specific experiment data updates.
   */
  onPocketExperimentUpdated(event, reason) {
    const value = lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {};
    // Loaded experiments are set up inside init()
    if (
      reason !== "feature-experiment-loaded" &&
      reason !== "feature-rollout-loaded"
    ) {
      this.store.dispatch(
        ac.BroadcastToContent({
          type: at.PREF_CHANGED,
          data: {
            name: "pocketConfig",
            value,
          },
        })
      );
    }
  }

  /**
   * Handler for when smart shortcuts experiment data updates.
   */
  onSmartShortcutsExperimentUpdated() {
    const value =
      lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {};
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "smartShortcutsConfig",
          value,
        },
      })
    );
  }

  /**
   * Handler for when inferred personalization experiment config values update.
   */
  onInferredPersonalizationExperimentUpdated() {
    const value =
      lazy.NimbusFeatures.newtabInferredPersonalization.getAllVariables() || {};
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "inferredPersonalizationConfig",
          value,
        },
      })
    );
  }

  /**
   * Handler for when widget experiment data updates.
   */
  onWidgetsUpdated() {
    const value = lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {};
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "widgetsConfig",
          value,
        },
      })
    );
  }

  /**
   * Handler for when OHTTP images experiment data updates.
   */
  onOhttpImagesUpdated() {
    const value = lazy.NimbusFeatures.newtabOhttpImages.getAllVariables() || {};
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "ohttpImagesConfig",
          value,
        },
      })
    );
  }

  /**
   * Handler for when adsBackend experiment data updates.
   */
  onAdsBackendUpdated() {
    const valueObj = this._getAdsBackendFeatures();

    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREF_CHANGED,
        data: {
          name: "adsBackendConfig",
          value: valueObj,
        },
      })
    );
  }

  init() {
    this._prefs.observeBranch(this);
    lazy.NimbusFeatures.newtab.onUpdate(this.onExperimentUpdated);
    lazy.NimbusFeatures.newtabTrainhop.onUpdate(
      this.onTrainhopExperimentUpdated
    );
    lazy.NimbusFeatures.pocketNewtab.onUpdate(this.onPocketExperimentUpdated);
    lazy.NimbusFeatures.newtabSmartShortcuts.onUpdate(
      this.onSmartShortcutsExperimentUpdated
    );
    lazy.NimbusFeatures.newtabInferredPersonalization.onUpdate(
      this.onInferredPersonalizationExperimentUpdated
    );
    lazy.NimbusFeatures.newtabWidgets.onUpdate(this.onWidgetsUpdated);
    lazy.NimbusFeatures.newtabOhttpImages.onUpdate(this.onOhttpImagesUpdated);
    /**
     * @backward-compat { version 149 }
     *
     * We can replace `adsBackend?` with `adsBackend` once 149 hits the
     * release channel.
     */
    lazy.NimbusFeatures.adsBackend?.onUpdate(this.onAdsBackendUpdated);

    // Get the initial value of each activity stream pref
    const values = {};
    for (const name of this._prefMap.keys()) {
      values[name] = this._prefs.get(name);
    }

    // These are not prefs, but are needed to determine stuff in content that can only be
    // computed in main process
    values.isPrivateBrowsingEnabled = lazy.PrivateBrowsingUtils.enabled;
    values.platform = AppConstants.platform;

    // Save the geo pref if we have it
    if (lazy.Region.home) {
      values.region = lazy.Region.home;
      this.geo = values.region;
    } else if (this.geo !== "") {
      // Watch for geo changes and use a dummy value for now
      Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
      this.geo = "";
    }

    // Get the firefox accounts url for links and to send firstrun metrics to.
    values.fxa_endpoint = Services.prefs.getStringPref(
      "browser.newtabpage.activity-stream.fxaccounts.endpoint",
      "https://accounts.firefox.com"
    );

    // Get the firefox update channel with values as default, nightly, beta or release
    values.appUpdateChannel = Services.prefs.getStringPref(
      "app.update.channel",
      ""
    );

    // Read the pref for search shortcuts top sites experiment from firefox.js and store it
    // in our internal list of prefs to watch
    let searchTopSiteExperimentPrefValue = Services.prefs.getBoolPref(
      "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"
    );
    values["improvesearch.topSiteSearchShortcuts"] =
      searchTopSiteExperimentPrefValue;
    this._prefMap.set("improvesearch.topSiteSearchShortcuts", {
      value: searchTopSiteExperimentPrefValue,
    });

    values.mayHaveSponsoredTopSites = Services.prefs.getBoolPref(
      "browser.topsites.useRemoteSetting"
    );

    // Add experiment values and default values
    values.featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {};
    values.pocketConfig =
      lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {};
    values.smartShortcutsConfig =
      lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {};
    values.widgetsConfig =
      lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {};
    values.trainhopConfig = this._getTrainhopConfig();
    values.adsBackendConfig = this._getAdsBackendFeatures();
    for (const { type, key, defaultValue } of PREF_DEFAULTS) {
      if (type === "bool") {
        this._setBoolPref(values, key, defaultValue);
      } else if (type === "string") {
        this._setStringPref(values, key, defaultValue);
      }
    }

    // Set the initial state of all prefs in redux
    this.store.dispatch(
      ac.BroadcastToContent({
        type: at.PREFS_INITIAL_VALUES,
        data: values,
        meta: {
          isStartup: true,
        },
      })
    );

    this.checkForActivationWindow(Temporal.Now.instant(), /* isStartup */ true);
  }

  uninit() {
    this.removeListeners();
  }

  removeListeners() {
    this._prefs.ignoreBranch(this);
    lazy.NimbusFeatures.newtab.offUpdate(this.onExperimentUpdated);
    lazy.NimbusFeatures.newtabTrainhop.offUpdate(
      this.onTrainhopExperimentUpdated
    );
    lazy.NimbusFeatures.pocketNewtab.offUpdate(this.onPocketExperimentUpdated);
    lazy.NimbusFeatures.newtabSmartShortcuts.offUpdate(
      this.onSmartShortcutsExperimentUpdated
    );
    lazy.NimbusFeatures.newtabInferredPersonalization.offUpdate(
      this.onInferredPersonalizationExperimentUpdated
    );
    lazy.NimbusFeatures.newtabWidgets.offUpdate(this.onWidgetsUpdated);
    lazy.NimbusFeatures.newtabOhttpImages.offUpdate(this.onOhttpImagesUpdated);
    /**
     * @backward-compat { version 149 }
     *
     * We can replace `adsBackend?` with `adsBackend` once 149 hits the
     * release channel.
     */
    lazy.NimbusFeatures.adsBackend?.offUpdate(this.onAdsBackendUpdated);

    if (this.geo === "") {
      Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
    }
  }

  /**
   * Checks whether the current profile is within the activation window and
   * updates the activation window state accordingly.
   *
   * @param {Temporal.Instant} [now=Temporal.Now.instant()] - The current time
   * @param {boolean} [isStartup=false] - Whether this is being called during
   *   browser startup. When true, forces re-application of default branch
   *   prefs even if already in the activation window state, since default
   *   branch prefs are not persisted across restarts.
   */
  checkForActivationWindow(now = Temporal.Now.instant(), isStartup = false) {
    const state = this.store.getState();
    if (!state || !state.Prefs) {
      return;
    }

    const { values } = state.Prefs;

    const {
      enabled = false,
      maxProfileAgeInHours = 48,
      disableTopSites = false,
      disableTopStories = false,
      variant = "",
      enterActivationWindowMessageID = "",
      exitActivationWindowMessageID = "",
    } = values?.trainhopConfig?.activationWindowBehavior ?? {};

    const { createdInstant } = lazy.AboutNewTab.activityStream;

    if (
      !enabled ||
      !createdInstant ||
      !variant ||
      lazy.SelectableProfileService.hasCreatedSelectableProfiles()
    ) {
      lazy.logConsole.log("Activation window evaluation skipped.");
      lazy.logConsole.debug(
        `enabled:${enabled}, createdInstant:${createdInstant}, variant: ${variant}`
      );
      if (this.inActivationWindowState) {
        lazy.logConsole.log(
          "Exiting activation window state because evaluation skipped."
        );
        this.exitActivationWindowState();
      }
      return;
    }

    // Are we within the maxProfileAgeInHours period? We compare against the
    // current instant and check to see if the creation time is before now,
    // but after maxProfileAgeInHours hours ago.
    const withinMaxProfileAgeInHours =
      Temporal.Instant.compare(createdInstant, now) === -1 &&
      Temporal.Instant.compare(
        createdInstant,
        now.subtract({ hours: maxProfileAgeInHours })
      ) === 1;

    if (withinMaxProfileAgeInHours) {
      lazy.logConsole.log(
        `Within activation window range (${maxProfileAgeInHours} hours)`
      );
      this.enterActivationWindowState(
        variant,
        disableTopSites,
        disableTopStories,
        enterActivationWindowMessageID,
        isStartup
      );
    } else if (this.inActivationWindowState) {
      this.exitActivationWindowState(exitActivationWindowMessageID);
    }
  }

  /**
   * Enters the activation window state by setting default branch prefs and
   * broadcasting the changes.
   *
   * @param {string} variant - The activation window variant identifier
   * @param {boolean} disableTopSites - Whether to disable top sites by default
   * @param {boolean} disableTopStories - Whether to disable top stories by default
   * @param {string} enterActivationWindowMessageID - Message ID to display when entering activation window
   * @param {boolean} [isStartup=false] - Whether this is being called during
   *   browser startup. When true, skips the idempotent check to ensure default
   *   branch prefs are reapplied.
   */
  enterActivationWindowState(
    variant,
    disableTopSites,
    disableTopStories,
    enterActivationWindowMessageID,
    isStartup = false
  ) {
    if (!isStartup && this.inActivationWindowState === variant) {
      lazy.logConsole.debug(
        `Already in activation window state for variant: ${variant}`
      );
      return;
    }

    lazy.logConsole.log("Entering activation window state");
    lazy.logConsole.debug(`variant: ${variant}`);

    // Clear the variant pref first to ensure we're not in activation window
    // state when the preference observer fires (even asynchronously) from the
    // default branch changes below.
    this._prefs.reset(ACTIVATION_WINDOW_VARIANT_PREF);

    const defaultBranch = Services.prefs.getDefaultBranch(
      this._prefs._branchStr
    );

    if (disableTopSites) {
      lazy.logConsole.log("Disabling top sites by default.");
      defaultBranch.setBoolPref(TOP_SITES_ENABLED_PREF, false);
      // This is a programmatic change, not a user action, so don't track it
      this.onPrefChanged(
        TOP_SITES_ENABLED_PREF,
        false,
        /* isUserChange */ false
      );
    }

    if (disableTopStories) {
      lazy.logConsole.log("Disabling top stories by default.");
      defaultBranch.setBoolPref(TOP_STORIES_ENABLED_PREF, false);
      // This is a programmatic change, not a user action, so don't track it
      this.onPrefChanged(
        TOP_STORIES_ENABLED_PREF,
        false,
        /* isUserChange */ false
      );
    }

    // Set the variant pref last, after default branch changes and broadcasts.
    // This prevents the preference observer from incorrectly tracking the
    // default branch changes as user actions.
    this._prefs.set(ACTIVATION_WINDOW_VARIANT_PREF, variant);

    // In the unlikely event that we've exited the activation window in the
    // past but somehow re-entered it, clear away the exit message pref so
    // that we don't accidentally show any exit messages.
    this._prefs.set(ACTIVATION_WINDOW_EXIT_MESSAGE_ID_PREF, "");

    // Set the enter message ID pref for ASRouter message targeting
    if (enterActivationWindowMessageID) {
      this._prefs.set(
        ACTIVATION_WINDOW_ENTER_MESSAGE_ID_PREF,
        enterActivationWindowMessageID
      );
    }

    lazy.logConsole.log("Activation window enter complete");
  }

  /**
   * Exits the activation window state by resetting default branch prefs and
   * restoring any user preference changes made during the window.
   *
   * @param {string} [exitActivationWindowMessageID=""] - Message ID to display when exiting activation window
   */
  exitActivationWindowState(exitActivationWindowMessageID = "") {
    lazy.logConsole.log("Exiting activation window state.", new Error().stack);
    this._prefs.reset(ACTIVATION_WINDOW_VARIANT_PREF);

    // Always reset defaults back to true first
    const defaultBranch = Services.prefs.getDefaultBranch(
      this._prefs._branchStr
    );
    lazy.logConsole.debug("Resetting default branch prefs to true");
    defaultBranch.setBoolPref(
      TOP_SITES_ENABLED_PREF,
      PREF_DEFAULT_VALUE_TOPSITES_ENABLED
    );
    defaultBranch.setBoolPref(
      TOP_STORIES_ENABLED_PREF,
      PREF_DEFAULT_VALUE_TOPSTORIES_ENABLED
    );

    // Check temp pref status
    const hasTopSitesTempPref = this._prefs.isSet(
      TOP_SITES_USER_VALUE_TEMP_PREF
    );
    const hasTopStoriesTempPref = this._prefs.isSet(
      TOP_STORIES_USER_VALUE_TEMP_PREF
    );
    lazy.logConsole.debug(
      `Temp prefs: topSites was disabled=${hasTopSitesTempPref}, topStories was disabled=${hasTopStoriesTempPref}`
    );

    // Broadcast the default changes (if user hasn't set these prefs, they'll see true)
    // If user has set them, setting user values below will trigger another broadcast
    if (!hasTopSitesTempPref) {
      lazy.logConsole.debug("Broadcasting top sites default change (true)");
      // This is a programmatic change, not a user action, so don't track it
      this.onPrefChanged(
        TOP_SITES_ENABLED_PREF,
        true,
        /* isUserChange */ false
      );
    } else {
      lazy.logConsole.debug(
        "Skipping top sites broadcast - will restore user value"
      );
    }
    if (!hasTopStoriesTempPref) {
      lazy.logConsole.debug("Broadcasting top stories default change (true)");
      // This is a programmatic change, not a user action, so don't track it
      this.onPrefChanged(
        TOP_STORIES_ENABLED_PREF,
        true,
        /* isUserChange */ false
      );
    } else {
      lazy.logConsole.debug(
        "Skipping top stories broadcast - will restore user value"
      );
    }

    // Then check if user made changes during activation window and apply them
    // Note: _prefs.set() will automatically trigger onPrefChanged() via the observer
    if (hasTopSitesTempPref) {
      const userValue = this._prefs.get(TOP_SITES_USER_VALUE_TEMP_PREF);
      lazy.logConsole.log(
        `Restoring user's top sites preference to ${userValue}`
      );
      this._prefs.set(TOP_SITES_ENABLED_PREF, userValue);
      this._prefs.reset(TOP_SITES_USER_VALUE_TEMP_PREF);
      lazy.logConsole.debug(
        "Top sites user value restored and temp pref cleared"
      );
    }

    if (hasTopStoriesTempPref) {
      const userValue = this._prefs.get(TOP_STORIES_USER_VALUE_TEMP_PREF);
      lazy.logConsole.log(
        `Restoring user's top stories preference to ${userValue}`
      );
      this._prefs.set(TOP_STORIES_ENABLED_PREF, userValue);
      this._prefs.reset(TOP_STORIES_USER_VALUE_TEMP_PREF);
      lazy.logConsole.debug(
        "Top stories user value restored and temp pref cleared"
      );
    }

    // Clear the enter message ID pref and set exit message ID for ASRouter
    this._prefs.set(ACTIVATION_WINDOW_ENTER_MESSAGE_ID_PREF, "");
    if (exitActivationWindowMessageID) {
      this._prefs.set(
        ACTIVATION_WINDOW_EXIT_MESSAGE_ID_PREF,
        exitActivationWindowMessageID
      );
    } else {
      this._prefs.set(ACTIVATION_WINDOW_EXIT_MESSAGE_ID_PREF, "");
    }

    lazy.logConsole.log("Activation window exit complete");
  }

  observe(subject, topic) {
    switch (topic) {
      case lazy.Region.REGION_TOPIC:
        this.store.dispatch(
          ac.BroadcastToContent({
            type: at.PREF_CHANGED,
            data: { name: "region", value: lazy.Region.home },
          })
        );
        break;
    }
  }

  onAction(action) {
    switch (action.type) {
      case at.INIT:
        this.init();
        break;
      case at.UNINIT:
        this.uninit();
        break;
      case at.CLEAR_PREF:
        Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name);
        break;
      case at.SET_PREF:
        this._prefs.set(action.data.name, action.data.value);
        break;
      case at.NEW_TAB_STATE_REQUEST: {
        this.checkForActivationWindow();
        break;
      }
    }
  }
}
