/* 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, {
  AddonStudies: "resource://normandy/lib/AddonStudies.sys.mjs",
  BranchedAddonStudyAction:
    "resource://normandy/actions/BranchedAddonStudyAction.sys.mjs",
  ExperimentManager: "resource://nimbus/lib/ExperimentManager.sys.mjs",
  PreferenceExperiments:
    "resource://normandy/lib/PreferenceExperiments.sys.mjs",
  RecipeRunner: "resource://normandy/lib/RecipeRunner.sys.mjs",
  RemoteSettingsExperimentLoader:
    "resource://nimbus/lib/RemoteSettingsExperimentLoader.sys.mjs",
});

const SHIELD_LEARN_MORE_URL_PREF = "app.normandy.shieldLearnMoreUrl";
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "gOptOutStudiesEnabled",
  "app.shield.optoutstudies.enabled"
);

/**
 * Class for managing an about: page that Normandy provides. Adapted from
 * browser/components/pocket/content/AboutPocket.sys.mjs.
 *
 * @implements nsIFactory
 * @implements nsIAboutModule
 */
class AboutPage {
  constructor({ chromeUrl, aboutHost, classID, description, uriFlags }) {
    this.chromeUrl = chromeUrl;
    this.aboutHost = aboutHost;
    this.classID = Components.ID(classID);
    this.description = description;
    this.uriFlags = uriFlags;
  }

  getURIFlags() {
    return this.uriFlags;
  }

  newChannel(uri, loadInfo) {
    const newURI = Services.io.newURI(this.chromeUrl);
    const channel = Services.io.newChannelFromURIWithLoadInfo(newURI, loadInfo);
    channel.originalURI = uri;

    if (this.uriFlags & Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT) {
      const principal = Services.scriptSecurityManager.createContentPrincipal(
        uri,
        {}
      );
      channel.owner = principal;
    }
    return channel;
  }
}
AboutPage.prototype.QueryInterface = ChromeUtils.generateQI(["nsIAboutModule"]);

/**
 * The module exported by this file.
 */
export let AboutPages = {};

/**
 * The weak set that keeps track of which browsing contexts
 * have an about:studies page.
 */
let BrowsingContexts = new WeakSet();
/**
 * about:studies page for displaying in-progress and past Shield studies.
 * @type {AboutPage}
 * @implements {nsIMessageListener}
 */
ChromeUtils.defineLazyGetter(AboutPages, "aboutStudies", () => {
  const aboutStudies = new AboutPage({
    chromeUrl: "resource://normandy-content/about-studies/about-studies.html",
    aboutHost: "studies",
    classID: "{6ab96943-a163-482c-9622-4faedc0e827f}",
    description: "Shield Study Listing",
    uriFlags:
      Ci.nsIAboutModule.ALLOW_SCRIPT |
      Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
      Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
      Ci.nsIAboutModule.IS_SECURE_CHROME_UI,
  });

  // Extra methods for about:study-specific behavior.
  Object.assign(aboutStudies, {
    getAddonStudyList() {
      return lazy.AddonStudies.getAll();
    },

    getPreferenceStudyList() {
      return lazy.PreferenceExperiments.getAll();
    },

    getMessagingSystemList() {
      return lazy.ExperimentManager.store.getAll();
    },

    async optInToExperiment(data) {
      try {
        await lazy.RemoteSettingsExperimentLoader.optInToExperiment(data);
        return {
          error: false,
          message: "Opt-in was successful.",
        };
      } catch (error) {
        return {
          error: true,
          message: error.message,
        };
      }
    },

    /** Add a browsing context to the weak set;
     * this weak set keeps track of all contexts
     * that are housing an about:studies page.
     */
    addToWeakSet(browsingContext) {
      BrowsingContexts.add(browsingContext);
    },
    /** Remove a browsing context to the weak set;
     * this weak set keeps track of all contexts
     * that are housing an about:studies page.
     */
    removeFromWeakSet(browsingContext) {
      BrowsingContexts.delete(browsingContext);
    },

    /**
     * Sends a message to every about:studies page,
     * by iterating over the BrowsingContexts weakset.
     * @param {string} message The message string to send to.
     * @param {object} data The data object to send.
     */
    _sendToAll(message, data) {
      ChromeUtils.nondeterministicGetWeakSetKeys(BrowsingContexts).forEach(
        browser =>
          browser.currentWindowGlobal
            .getActor("ShieldFrame")
            .sendAsyncMessage(message, data)
      );
    },

    /**
     * Get if studies are enabled. This has to be in the parent process,
     * since RecipeRunner is stateful, and can't be interacted with from
     * content processes safely.
     */
    async getStudiesEnabled() {
      await lazy.RecipeRunner.initializedPromise.promise;
      return lazy.RecipeRunner.enabled && lazy.gOptOutStudiesEnabled;
    },

    /**
     * Disable an active add-on study and remove its add-on.
     * @param {String} recipeId the id of the addon to remove
     * @param {String} reason the reason for removal
     */
    async removeAddonStudy(recipeId, reason) {
      try {
        const action = new lazy.BranchedAddonStudyAction();
        await action.unenroll(recipeId, reason);
      } catch (err) {
        // If the exception was that the study was already removed, that's ok.
        // If not, rethrow the error.
        if (!err.toString().includes("already inactive")) {
          throw err;
        }
      } finally {
        // Update any open tabs with the new study list now that it has changed,
        // even if the above failed.
        this.getAddonStudyList().then(list =>
          this._sendToAll("Shield:UpdateAddonStudyList", list)
        );
      }
    },

    /**
     * Disable an active preference study.
     * @param {String} experimentName the name of the experiment to remove
     * @param {String} reason the reason for removal
     */
    async removePreferenceStudy(experimentName, reason) {
      try {
        await lazy.PreferenceExperiments.stop(experimentName, {
          reason,
          caller: "AboutPages.removePreferenceStudy",
        });
      } catch (err) {
        // If the exception was that the study was already removed, that's ok.
        // If not, rethrow the error.
        if (!err.toString().includes("already expired")) {
          throw err;
        }
      } finally {
        // Update any open tabs with the new study list now that it has changed,
        // even if the above failed.
        this.getPreferenceStudyList().then(list =>
          this._sendToAll("Shield:UpdatePreferenceStudyList", list)
        );
      }
    },

    async removeMessagingSystemExperiment(slug, reason) {
      lazy.ExperimentManager.unenroll(slug, reason);
      this._sendToAll(
        "Shield:UpdateMessagingSystemExperimentList",
        lazy.ExperimentManager.store.getAll()
      );
    },

    openDataPreferences() {
      const browserWindow =
        Services.wm.getMostRecentWindow("navigator:browser");
      browserWindow.openPreferences("privacy-reports");
    },

    getShieldLearnMoreHref() {
      return Services.urlFormatter.formatURLPref(SHIELD_LEARN_MORE_URL_PREF);
    },
  });

  return aboutStudies;
});
