/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
  isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

const OPTED_IN_PREF = "browser.shopping.experience2023.optedIn";
const ACTIVE_PREF = "browser.shopping.experience2023.active";
const LAST_AUTO_ACTIVATE_PREF =
  "browser.shopping.experience2023.lastAutoActivate";
const AUTO_ACTIVATE_COUNT_PREF =
  "browser.shopping.experience2023.autoActivateCount";
const ADS_USER_ENABLED_PREF = "browser.shopping.experience2023.ads.userEnabled";
const AUTO_OPEN_ENABLED_PREF =
  "browser.shopping.experience2023.autoOpen.enabled";
const AUTO_OPEN_USER_ENABLED_PREF =
  "browser.shopping.experience2023.autoOpen.userEnabled";
const SIDEBAR_CLOSED_COUNT_PREF =
  "browser.shopping.experience2023.sidebarClosedCount";

const CFR_FEATURES_PREF =
  "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";

export const ShoppingUtils = {
  initialized: false,
  registered: false,
  handledAutoActivate: false,
  nimbusEnabled: false,
  nimbusControl: false,

  _updateNimbusVariables() {
    this.nimbusEnabled =
      lazy.NimbusFeatures.shopping2023.getVariable("enabled");
    this.nimbusControl =
      lazy.NimbusFeatures.shopping2023.getVariable("control");
  },

  onNimbusUpdate() {
    this._updateNimbusVariables();
    if (this.nimbusEnabled) {
      ShoppingUtils.init();
      Glean.shoppingSettings.nimbusDisabledShopping.set(false);
    } else {
      ShoppingUtils.uninit();
      Glean.shoppingSettings.nimbusDisabledShopping.set(true);
    }
  },

  // Runs once per session:
  // * at application startup, with startup idle tasks,
  // * or after the user is enrolled in the Nimbus experiment.
  init() {
    if (this.initialized) {
      return;
    }
    this.onNimbusUpdate = this.onNimbusUpdate.bind(this);
    this.onActiveUpdate = this.onActiveUpdate.bind(this);

    if (!this.registered) {
      // Note (bug 1855545): we must set `this.registered` before calling
      // `onUpdate`, as it will immediately invoke `this.onNimbusUpdate`,
      // which in turn calls `ShoppingUtils.init`, creating an infinite loop.
      this.registered = true;
      lazy.NimbusFeatures.shopping2023.onUpdate(this.onNimbusUpdate);
      this._updateNimbusVariables();
    }

    if (!this.nimbusEnabled) {
      return;
    }

    // Do startup-time stuff here, like recording startup-time glean events
    // or adjusting onboarding-related prefs once per session.

    this.setOnUpdate(undefined, undefined, this.optedIn);
    this.recordUserAdsPreference();
    this.recordUserAutoOpenPreference();

    if (this._isAutoOpenEligible()) {
      Services.prefs.setBoolPref(ACTIVE_PREF, true);
    }
    Services.prefs.addObserver(ACTIVE_PREF, this.onActiveUpdate);

    Services.prefs.setIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0);

    this.initialized = true;
  },

  // Runs once per session:
  // * when the user is unenrolled from the Nimbus experiment,
  // * or at shutdown, after quit-application-granted.
  uninit() {
    if (!this.initialized) {
      return;
    }

    // Do shutdown-time stuff here, like firing glean pings or modifying any
    // prefs for onboarding.

    Services.prefs.removeObserver(ACTIVE_PREF, this.onActiveUpdate);

    this.initialized = false;
  },

  isProductPageNavigation(aLocationURI, aFlags) {
    if (!lazy.isProductURL(aLocationURI)) {
      return false;
    }

    // Ignore same-document navigation, except in the case of Walmart
    // as they use pushState to navigate between pages.
    let isWalmart = aLocationURI.host.includes("walmart");
    let isNewDocument = !aFlags;

    let isSameDocument =
      aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT;
    let isReload = aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_RELOAD;
    let isSessionRestore =
      aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SESSION_STORE;

    // Unfortunately, Walmart sometimes double-fires history manipulation
    // events when navigating between product pages. To dedupe, cache the
    // last visited Walmart URL just for a few milliseconds, so we can avoid
    // double-counting such navigations.
    if (isWalmart) {
      if (
        this.lastWalmartURI &&
        aLocationURI.equalsExceptRef(this.lastWalmartURI)
      ) {
        return false;
      }
      this.lastWalmartURI = aLocationURI;
      lazy.setTimeout(() => {
        this.lastWalmartURI = null;
      }, 100);
    }

    return (
      // On initial visit to a product page, even from another domain, both a page
      // load and a pushState will be triggered by Walmart, so this will
      // capture only a single displayed event.
      (!isWalmart && (isNewDocument || isReload || isSessionRestore)) ||
      (isWalmart && isSameDocument)
    );
  },

  // For users in either the nimbus control or treatment groups, increment a
  // counter when they visit supported product pages.
  recordExposure() {
    if (this.nimbusEnabled || this.nimbusControl) {
      Glean.shopping.productPageVisits.add(1);
    }
  },

  setOnUpdate(_pref, _prev, current) {
    Glean.shoppingSettings.componentOptedOut.set(current === 2);
    Glean.shoppingSettings.hasOnboarded.set(current > 0);
  },

  recordUserAdsPreference() {
    Glean.shoppingSettings.disabledAds.set(!ShoppingUtils.adsUserEnabled);
  },

  recordUserAutoOpenPreference() {
    Glean.shoppingSettings.autoOpenUserDisabled.set(
      !ShoppingUtils.autoOpenUserEnabled
    );
  },

  /**
   * If the user has not opted in, automatically set the sidebar to `active` if:
   * 1. The sidebar has not already been automatically set to `active` twice.
   * 2. It's been at least 24 hours since the user last saw the sidebar because
   *    of this auto-activation behavior.
   * 3. This method has not already been called (handledAutoActivate is false)
   */
  handleAutoActivateOnProduct() {
    if (!this.handledAutoActivate && !this.optedIn && this.cfrFeatures) {
      let autoActivateCount = Services.prefs.getIntPref(
        AUTO_ACTIVATE_COUNT_PREF,
        0
      );
      let lastAutoActivate = Services.prefs.getIntPref(
        LAST_AUTO_ACTIVATE_PREF,
        0
      );
      let now = Date.now() / 1000;
      // If we automatically set `active` to true in a previous session less
      // than 24 hours ago, set it to false now. This is done to prevent the
      // auto-activation state from persisting between sessions. Effectively,
      // the auto-activation will persist until either 1) the sidebar is closed,
      // or 2) Firefox restarts.
      if (now - lastAutoActivate < 24 * 60 * 60) {
        Services.prefs.setBoolPref(ACTIVE_PREF, false);
      }
      // Set active to true if we haven't done so recently nor more than twice.
      else if (autoActivateCount < 2) {
        Services.prefs.setBoolPref(ACTIVE_PREF, true);
        Services.prefs.setIntPref(
          AUTO_ACTIVATE_COUNT_PREF,
          autoActivateCount + 1
        );
        Services.prefs.setIntPref(LAST_AUTO_ACTIVATE_PREF, now);
      }
    }
    this.handledAutoActivate = true;
  },

  /**
   * Send a Shopping-related trigger message to ASRouter.
   *
   * @param {object} trigger The trigger object to send to ASRouter.
   * @param {object} trigger.context Additional trigger properties to pass to
   *   the targeting context.
   * @param {string} trigger.id The id of the trigger.
   * @param {MozBrowser} trigger.browser The browser to associate with the
   *   trigger. (This can determine the tab/window the message is shown in,
   *   depending on the message surface)
   */
  async sendTrigger(trigger) {
    await lazy.ASRouter.waitForInitialized;
    await lazy.ASRouter.sendTriggerMessage(trigger);
  },

  onActiveUpdate(subject, topic, data) {
    if (data !== ACTIVE_PREF || topic !== "nsPref:changed") {
      return;
    }

    let newValue = Services.prefs.getBoolPref(ACTIVE_PREF);
    if (newValue === false) {
      ShoppingUtils.resetActiveOnNextProductPage = true;
    }
  },

  _isAutoOpenEligible() {
    return (
      this.optedIn === 1 && this.autoOpenEnabled && this.autoOpenUserEnabled
    );
  },

  onLocationChange(aLocationURI, aFlags) {
    let isProductPageNavigation = this.isProductPageNavigation(
      aLocationURI,
      aFlags
    );

    if (isProductPageNavigation) {
      this.recordExposure(aLocationURI, aFlags);
    }

    if (
      this._isAutoOpenEligible() &&
      this.resetActiveOnNextProductPage &&
      isProductPageNavigation
    ) {
      this.resetActiveOnNextProductPage = false;
      Services.prefs.setBoolPref(ACTIVE_PREF, true);
    }
  },
};

XPCOMUtils.defineLazyPreferenceGetter(
  ShoppingUtils,
  "optedIn",
  OPTED_IN_PREF,
  0,
  ShoppingUtils.setOnUpdate
);

XPCOMUtils.defineLazyPreferenceGetter(
  ShoppingUtils,
  "cfrFeatures",
  CFR_FEATURES_PREF,
  true
);

XPCOMUtils.defineLazyPreferenceGetter(
  ShoppingUtils,
  "adsUserEnabled",
  ADS_USER_ENABLED_PREF,
  false,
  ShoppingUtils.recordUserAdsPreference
);

XPCOMUtils.defineLazyPreferenceGetter(
  ShoppingUtils,
  "autoOpenEnabled",
  AUTO_OPEN_ENABLED_PREF,
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  ShoppingUtils,
  "autoOpenUserEnabled",
  AUTO_OPEN_USER_ENABLED_PREF,
  false,
  ShoppingUtils.recordUserAutoOpenPreference
);
