/* 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";
import { LoginHelper } from "resource://gre/modules/LoginHelper.sys.mjs";
import { DataSourceBase } from "resource://gre/modules/megalist/aggregator/datasources/DataSourceBase.sys.mjs";
import { LoginCSVImport } from "resource://gre/modules/LoginCSVImport.sys.mjs";
import { LoginExport } from "resource://gre/modules/LoginExport.sys.mjs";
import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";

const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
  UIState: "resource://services-sync/UIState.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "BREACH_ALERTS_ENABLED",
  "signon.management.page.breach-alerts.enabled",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "PASSWORD_SYNC_ENABLED",
  "services.sync.engine.passwords",
  false
);

// Precedence values when sorting logins by alerts.
const ALERT_VALUES = {
  breached: 0,
  vulnerable: 1,
  noUsername: 2,
  none: 3,
};

export const DISPLAY_MODES = {
  ALERTS: "DisplayAlerts",
  ALL: "DisplayAll",
};

const VIEW_MODES = {
  LIST: "List",
  ADD: "Add",
  EDIT: "Edit",
};

export const SUPPORT_URL =
  Services.urlFormatter.formatURLPref("app.support.baseURL") +
  "password-manager-remember-delete-edit-logins";

export const PREFERENCES_URL = "about:preferences#privacy-logins";

const ORIGIN_BREACHED_URL =
  "https://support.mozilla.org/en-US/kb/firefox-password-manager-alerts-breached-websites";

const VULNERABLE_PASSWORD_URL =
  "https://support.mozilla.org/en-US/kb/create-secure-passwords-keep-your-identity-safe";

const IMPORT_FILE_SUPPORT_URL =
  "https://support.mozilla.org/kb/import-login-data-file";

const IMPORT_FILE_REPORT_URL = "about:loginsimportreport";

/**
 * Data source for Logins.
 *
 * Each login is represented by 3 lines: origin, username and password.
 *
 * Protypes are used to reduce memory need because for different records
 * similar lines will differ in values only.
 */
export class LoginDataSource extends DataSourceBase {
  doneReloadDataSource;
  #originPrototype;
  #usernamePrototype;
  #passwordPrototype;
  #header;
  #initialized;
  #exportPasswordsStrings;
  #displayMode;

  constructor(...args) {
    super(...args);
    // Wait for Fluent to provide strings before loading data
    this.localizeStrings({
      originLabel: { id: "contextual-manager-passwords-origin-label" },
      usernameLabel: { id: "contextual-manager-passwords-username-label" },
      passwordLabel: { id: "contextual-manager-passwords-password-label" },
      revealPasswordOSAuthDialogPrompt: {
        id: this.getPlatformFtl(
          "contextual-manager-passwords-reveal-password-os-auth-dialog-message"
        ),
      },
      copyPasswordOSAuthDialogPrompt: {
        id: this.getPlatformFtl(
          "contextual-manager-passwords-copy-password-os-auth-dialog-message"
        ),
      },
      editPasswordOSAuthDialogPrompt: {
        id: this.getPlatformFtl(
          "contextual-manager-passwords-edit-password-os-auth-dialog-message"
        ),
      },
      passwordOSAuthDialogCaption: {
        id: "contextual-manager-passwords-os-auth-dialog-caption",
      },
      passwordsImportFilePickerTitle: {
        id: "contextual-manager-passwords-import-file-picker-title",
      },
      passwordsImportFilePickerImportButton: {
        id: "contextual-manager-passwords-import-file-picker-import-button",
      },
      passwordsImportFilePickerCsvFilterTitle: {
        id: "contextual-manager-passwords-import-file-picker-csv-filter-title",
      },
      passwordsImportFilePickerTsvFilterTitle: {
        id: "contextual-manager-passwords-import-file-picker-tsv-filter-title",
      },
      exportPasswordsOSReauthMessage: {
        id: this.getPlatformFtl(
          "contextual-manager-passwords-export-os-auth-dialog-message"
        ),
      },
      passwordsExportFilePickerTitle: {
        id: "contextual-manager-passwords-export-file-picker-title",
      },
      passwordsExportFilePickerDefaultFileName: {
        id: "contextual-manager-passwords-export-file-picker-default-filename",
      },
      passwordsExportFilePickerExportButton: {
        id: "contextual-manager-passwords-export-file-picker-export-button",
      },
      passwordsExportFilePickerCsvFilterTitle: {
        id: "contextual-manager-passwords-export-file-picker-csv-filter-title",
      },
    }).then(strings => {
      const copyCommand = { id: "Copy" };
      const editCommand = { id: "Edit" };
      const deleteCommand = { id: "Delete" };
      const dismissBreachCommand = {
        id: "DismissBreach",
      };
      this.#header = this.createHeaderLine();
      this.#header.commands.push(
        { id: "AddLogin" },
        { id: "UpdateLogin" },
        { id: "DeleteLogin" },
        { id: "DiscardChanges" },
        { id: "ConfirmDiscardChanges" },
        {
          id: "ImportFromBrowser",
        },
        { id: "Import" },
        { id: "Export" },
        { id: "RemoveAll" },
        {
          id: "Settings",
          url: PREFERENCES_URL,
        },
        { id: "Help", url: SUPPORT_URL },
        { id: "UpdateDisplayMode" },
        { id: "OpenLink" }
      );
      this.#header.executeImport = async () =>
        this.#importFromFile(
          strings.passwordsImportFilePickerTitle,
          strings.passwordsImportFilePickerImportButton,
          strings.passwordsImportFilePickerCsvFilterTitle,
          strings.passwordsImportFilePickerTsvFilterTitle
        );

      this.#header.executeOpenLink = url => this.#openLink(url);
      this.#header.executeImportFromBrowser = () => this.#importFromBrowser();
      this.#header.executeRemoveAll = () => this.#removeAllPasswords();
      this.#header.executeExport = async () => this.#exportLogins();
      this.#header.executeAddLogin = newLogin => this.#addLogin(newLogin);
      this.#header.executeUpdateLogin = login => this.#updateLogin(login);
      this.#header.executeDeleteLogin = login => this.#deleteLogin(login);
      this.#header.executeDiscardChanges = options => this.#cancelEdit(options);
      this.#header.executeConfirmDiscardChanges = () =>
        this.discardChangesConfirmed();

      this.#exportPasswordsStrings = {
        OSReauthMessage: strings.exportPasswordsOSReauthMessage,
        OSAuthDialogCaption: strings.passwordOSAuthDialogCaption,
        ExportFilePickerTitle: strings.passwordsExportFilePickerTitle,
        FilePickerExportButton: strings.passwordsExportFilePickerExportButton,
        FilePickerDefaultFileName:
          strings.passwordsExportFilePickerDefaultFileName.concat(".csv"),
        FilePickerCsvFilterTitle:
          strings.passwordsExportFilePickerCsvFilterTitle,
      };

      this.#header.executeUpdateDisplayMode = displayMode => {
        if (this.#displayMode !== displayMode) {
          this.#displayMode = displayMode;
          this.#reloadDataSource();
          this.setDisplayMode(this.#displayMode);
        }
      };

      const openOriginInNewTab = origin => this.#openLink(origin);

      this.#originPrototype = this.prototypeDataLine({
        field: { value: "origin" },
        label: { value: strings.originLabel },
        start: { value: true },
        value: {
          get() {
            return this.record.displayOrigin;
          },
        },
        valueIcon: {
          get() {
            return `page-icon:${this.record.origin}`;
          },
        },
        href: {
          get() {
            return this.record.origin;
          },
        },
        breachedNotification: {
          value: {
            id: "breached-origin-warning",
            url: ORIGIN_BREACHED_URL,
          },
        },
        commands: {
          value: [
            { id: "Open" },
            copyCommand,
            editCommand,
            deleteCommand,
            dismissBreachCommand,
          ],
        },
        executeDismissBreach: {
          value() {
            lazy.LoginBreaches.recordBreachAlertDismissal(this.record.guid);
            delete this.breached;
            this.refreshOnScreen();
          },
        },
        executeCopy: {
          value() {
            this.copyToClipboard(this.record.origin);
          },
        },
        executeDelete: {
          value() {
            this.setLayout({ id: "remove-login" });
          },
        },
        executeOpenLink: {
          value() {
            openOriginInNewTab(this.record.origin);
          },
        },
      });
      this.#usernamePrototype = this.prototypeDataLine({
        field: { value: "username" },
        label: { value: strings.usernameLabel },
        value: {
          get() {
            return this.editingValue ?? this.record.username;
          },
        },
        noUsernameNotification: {
          value: {
            id: "no-username-warning",
          },
        },
        commands: { value: [copyCommand, editCommand, "-", deleteCommand] },
        executeEdit: {
          value() {
            this.editingValue = this.record.username ?? "";
            this.refreshOnScreen();
          },
        },
      });
      this.#passwordPrototype = this.prototypeDataLine({
        field: { value: "password" },
        label: { value: strings.passwordLabel },
        concealed: { value: true, writable: true },
        end: { value: true },
        value: {
          get() {
            return (
              this.editingValue ??
              (this.concealed ? "••••••••" : this.record.password)
            );
          },
        },
        vulnerableNotification: {
          value: {
            id: "vulnerable-password-warning",
            url: VULNERABLE_PASSWORD_URL,
          },
        },
        commands: {
          value: [
            {
              ...copyCommand,
              verify: true,
              OSAuthPromptMessage: strings.copyPasswordOSAuthDialogPrompt,
              OSAuthCaptionMessage: strings.passwordOSAuthDialogCaption,
            },
            {
              id: "Reveal",
              verify: true,
              OSAuthPromptMessage: strings.revealPasswordOSAuthDialogPrompt,
              OSAuthCaptionMessage: strings.passwordOSAuthDialogCaption,
            },
            { id: "Conceal" },
            {
              ...editCommand,
              verify: true,
              OSAuthPromptMessage: strings.editPasswordOSAuthDialogPrompt,
              OSAuthCaptionMessage: strings.passwordOSAuthDialogCaption,
            },
            deleteCommand,
            { id: "Cancel" },
          ],
        },
        executeReveal: {
          value() {
            this.concealed = false;
            clearTimeout(this.concealPasswordTimeout);
            this.concealPasswordTimeout = setTimeout(
              () => this.executeConceal(),
              AUTH_TIMEOUT_MS
            );
            this.refreshOnScreen();
          },
        },
        executeConceal: {
          value() {
            this.concealed = true;
            this.refreshOnScreen();
          },
        },
        executeCopy: {
          value() {
            this.copyToClipboard(this.record.password);
          },
        },
        executeEdit: {
          value() {
            this.editingValue = this.record.password ?? "";
            this.refreshOnScreen();
          },
        },
      });

      // Sort by origin, then by username, then by GUID
      this.#displayMode = DISPLAY_MODES.ALL;
      this.#addObservers();
      this.#reloadDataSource();
      this.#initialized = true;
    });
  }

  willDestroy() {
    this.#removeObservers();
  }

  #addObservers() {
    Services.obs.addObserver(this, "passwordmgr-storage-changed");
    Services.obs.addObserver(this, "passwordmgr-crypto-login");
    Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled");

    Services.prefs.addObserver("signon.rememberSignons", this);
    Services.prefs.addObserver(
      "signon.management.page.breach-alerts.enabled",
      this
    );
    Services.prefs.addObserver(
      "signon.management.page.vulnerable-passwords.enabled",
      this
    );
  }

  #removeObservers() {
    Services.obs.removeObserver(this, "passwordmgr-storage-changed");
    Services.obs.removeObserver(this, "passwordmgr-crypto-login");
    Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");

    Services.prefs.removeObserver("signon.rememberSignons", this);
    Services.prefs.removeObserver(
      "signon.management.page.breach-alerts.enabled",
      this
    );
    Services.prefs.removeObserver(
      "signon.management.page.vulnerable-passwords.enabled",
      this
    );
  }

  #recordLoginsUpdate(changeType) {
    Glean.contextualManager.recordsUpdate.record({
      change_type: changeType,
    });
  }

  async #importFromFile(title, buttonLabel, csvTitle, tsvTitle) {
    const { BrowserWindowTracker } = ChromeUtils.importESModule(
      "resource:///modules/BrowserWindowTracker.sys.mjs"
    );
    const browsingContext = BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    }).browsingContext;
    let { result, path } = await this.openFilePickerDialog(
      title,
      buttonLabel,
      [
        {
          title: csvTitle,
          extensionPattern: "*.csv",
        },
        {
          title: tsvTitle,
          extensionPattern: "*.tsv",
        },
      ],
      browsingContext
    );

    if (result != Ci.nsIFilePicker.returnCancel) {
      try {
        const summary = await LoginCSVImport.importFromCSV(path);
        const counts = { added: 0, modified: 0, no_change: 0, error: 0 };

        for (const item of summary) {
          const type = item.result;
          if (type.includes("error")) {
            counts.error++;
          } else {
            counts[type]++;
          }
        }

        this.setNotification({
          id: "import-success",
          l10nArgs: counts,
          url: IMPORT_FILE_REPORT_URL,
        });

        this.#recordLoginsUpdate("import");
      } catch (e) {
        if (e.result !== Cr.NS_ERROR_ABORT) {
          this.setNotification({
            id: "import-error",
            url: IMPORT_FILE_SUPPORT_URL,
            commands: {
              onRetry: "Import",
            },
          });
        }
      }
    }
  }

  async openFilePickerDialog(
    title,
    okButtonLabel,
    appendFilters,
    browsingContext
  ) {
    return new Promise(resolve => {
      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
      fp.init(browsingContext, title, Ci.nsIFilePicker.modeOpen);
      for (const appendFilter of appendFilters) {
        fp.appendFilter(appendFilter.title, appendFilter.extensionPattern);
      }
      fp.appendFilters(Ci.nsIFilePicker.filterAll);
      fp.okButtonLabel = okButtonLabel;
      fp.open(async result => {
        resolve({ result, path: fp.file ? fp.file.path : "" });
      });
    });
  }

  #importFromBrowser() {
    const { BrowserWindowTracker } = ChromeUtils.importESModule(
      "resource:///modules/BrowserWindowTracker.sys.mjs"
    );
    const browser = BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    }).gBrowser;
    try {
      lazy.MigrationUtils.showMigrationWizard(browser.documentGlobal, {
        entrypoint: lazy.MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS,
      });
    } catch (ex) {
      console.error(ex);
    }
  }

  #isPasswordSyncEnabled() {
    const state = lazy.UIState.get();
    return state.syncEnabled && lazy.PASSWORD_SYNC_ENABLED;
  }

  async #removeAllPasswords() {
    const { total } = this.#header.value;
    const messageId = this.#isPasswordSyncEnabled()
      ? "contextual-manager-passwords-remove-all-message-sync"
      : "contextual-manager-passwords-remove-all-message";

    const { confirmed, checked } = await this.#showWindowPrompt(
      { id: "contextual-manager-passwords-remove-all-title", args: { total } },
      { id: messageId, args: { total } },
      {
        id: "contextual-manager-passwords-remove-all-confirm-button",
        args: { total },
      },
      {
        id: "contextual-manager-passwords-remove-all-passwords-checkbox",
        args: { total },
      }
    );

    if (!confirmed || !checked) {
      return;
    }

    if (confirmed) {
      await Services.logins.removeAllLoginsAsync();
      this.setNotification({
        id: "delete-login-success",
        l10nArgs: { total },
        viewMode: VIEW_MODES.LIST,
      });
      this.#recordLoginsUpdate("remove_all");
    }
  }

  async confirmRemoveLogin([record]) {
    await Services.logins.removeLoginAsync(record);
    this.cancelDialog();
  }

  confirmRetryImport() {
    this.#header.executeImport();
    this.cancelDialog();
  }

  async #exportLogins() {
    const { BrowserWindowTracker } = ChromeUtils.importESModule(
      "resource:///modules/BrowserWindowTracker.sys.mjs"
    );
    const browsingContext = BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    }).browsingContext;

    const reason = "export_cpm";
    let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth(
      browsingContext,
      null, // Prompt regardless of a recent prompt
      this.#exportPasswordsStrings.OSReauthMessage,
      this.#exportPasswordsStrings.OSAuthDialogCaption,
      reason
    );

    let { name, extra = {}, value = null } = telemetryEvent;
    if (value) {
      extra.value = value;
    }
    Glean.pwmgr[name].record(extra);

    if (!isAuthorized) {
      return;
    }

    const { confirmed } = await this.#showWindowPrompt(
      { id: "contextual-manager-export-passwords-dialog-title" },
      { id: "contextual-manager-export-passwords-dialog-message" },
      { id: "contextual-manager-export-passwords-dialog-confirm-button" }
    );

    if (confirmed) {
      this.exportFilePickerDialog(browsingContext);
    }
  }

  exportFilePickerDialog(browsingContext) {
    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    const fpCallback = aResult => {
      if (aResult != Ci.nsIFilePicker.returnCancel) {
        LoginExport.exportAsCSV(fp.file.path);
        Glean.pwmgr.mgmtMenuItemUsedExportComplete.record();
        this.setNotification({
          id: "export-passwords-success",
        });
      }
    };
    fp.init(
      browsingContext,
      this.#exportPasswordsStrings.ExportFilePickerTitle,
      Ci.nsIFilePicker.modeSave
    );
    fp.appendFilter(
      this.#exportPasswordsStrings.FilePickerCsvFilterTitle,
      "*.csv"
    );
    fp.appendFilters(Ci.nsIFilePicker.filterAll);
    fp.defaultString = this.#exportPasswordsStrings.FilePickerDefaultFileName;
    fp.defaultExtension = "csv";
    fp.okButtonLabel = this.#exportPasswordsStrings.FilePickerExportButton;
    fp.open(fpCallback);
  }

  #openLink(url) {
    const { BrowserWindowTracker } = ChromeUtils.importESModule(
      "resource:///modules/BrowserWindowTracker.sys.mjs"
    );
    const browser = BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    }).gBrowser;
    browser.documentGlobal.switchToTabHavingURI(url, true, {
      ignoreFragment: "whenComparingAndReplace",
    });
  }

  async #showWindowPrompt(
    titleL10n,
    messageL10n,
    confirmButtonL10n,
    checkboxL10n = null
  ) {
    const { BrowserWindowTracker } = ChromeUtils.importESModule(
      "resource:///modules/BrowserWindowTracker.sys.mjs"
    );
    const win = BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    });

    const { title, message, confirmButton } = await this.localizeStrings({
      title: titleL10n,
      message: messageL10n,
      confirmButton: confirmButtonL10n,
    });

    let checkbox = null;
    if (checkboxL10n) {
      const { checkboxArg } = await this.localizeStrings({
        checkboxArg: checkboxL10n,
      });
      checkbox = checkboxArg;
    }

    // For more context on what these flags mean:
    // https://firefox-source-docs.mozilla.org/toolkit/components/prompts/prompts/nsIPromptService-reference.html#Prompter.confirmEx
    const flags =
      Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
      Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;

    const result = await Services.prompt.asyncConfirmEx(
      win.browsingContext,
      Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
      title,
      message,
      flags,
      confirmButton,
      null,
      null,
      checkbox,
      false,
      {}
    );

    const propBag = result.QueryInterface(Ci.nsIPropertyBag2);
    const options = {
      // buttonNumClicked will be:
      //  - 0 for confirm
      //  - 1 for cancelling/declining.
      confirmed: propBag.get("buttonNumClicked") == 0,
      checked: propBag.get("checked"),
    };

    return options;
  }

  async #addLogin(newLogin) {
    const origin = LoginHelper.getLoginOrigin(newLogin.origin);
    newLogin.origin = origin;
    Object.assign(newLogin, {
      formActionOrigin: "",
      usernameField: "",
      passwordField: "",
    });
    newLogin = LoginHelper.vanillaObjectToLogin(newLogin);
    try {
      newLogin = await Services.logins.addLoginAsync(newLogin);
      this.setNotification({
        id: "add-login-success",
        l10nArgs: { url: origin },
        guid: newLogin.guid,
        viewMode: VIEW_MODES.LIST,
      });
      this.#recordLoginsUpdate("add");
    } catch (error) {
      this.#handleLoginStorageErrors(origin, error);
    }
  }

  async #updateLogin(login) {
    const logins = await Services.logins.searchLoginsAsync({
      origin: login.origin,
      guid: login.guid,
    });
    if (logins.length != 1) {
      return;
    }

    let notificationId = "update-login-success";
    const modifiedLogin = logins[0].clone();

    if (login.hasOwnProperty("username")) {
      const passwordModified = modifiedLogin.password !== login.password;
      const usernameModified = modifiedLogin.username !== login.username;

      if (!passwordModified && usernameModified) {
        notificationId = "update-username-success";
      }
      modifiedLogin.username = login.username;
    }

    if (login.hasOwnProperty("password")) {
      modifiedLogin.password = login.password;
    }

    try {
      await Services.logins.modifyLoginAsync(logins[0], modifiedLogin);
      this.setNotification({
        id: notificationId,
        viewMode: VIEW_MODES.LIST,
      });
      this.#recordLoginsUpdate("edit");
    } catch (error) {
      this.#handleLoginStorageErrors(modifiedLogin.origin, error);
    }
  }

  #cancelEdit() {
    this.setNotification({ id: "discard-changes" });
  }

  #handleLoginStorageErrors(origin, error) {
    if (error.message.includes("This login already exists")) {
      const existingLoginGuid = error.data.toString();
      this.setNotification({
        id: "login-already-exists-warning",
        l10nArgs: { url: origin },
        guid: existingLoginGuid,
      });
    }
  }

  async #deleteLogin(login) {
    const logins = await Services.logins.searchLoginsAsync({
      origin: login.origin,
      guid: login.guid,
    });
    if (logins.length != 1) {
      return;
    }
    await Services.logins.removeLoginAsync(logins[0]);
    this.setNotification({
      id: "delete-login-success",
      l10nArgs: { total: 1 },
      viewMode: VIEW_MODES.LIST,
    });
    this.#recordLoginsUpdate("remove");
  }

  /**
   * Enumerate all the lines provided by this data source.
   *
   * @param {string} searchText used to filter data
   */
  *enumerateLines(searchText) {
    if (!this.#initialized) {
      // Async Fluent API makes it possible to have data source waiting
      // for the localized strings, which can be detected by undefined in #enabled.
      return;
    }

    yield this.#header;

    const stats = { count: 0, total: 0 };
    searchText = searchText.toUpperCase();
    yield* this.enumerateLinesForMatchingRecords(
      searchText,
      stats,
      login =>
        login.origin.toUpperCase().includes(searchText) ||
        login.username.toUpperCase().includes(searchText) ||
        login.password.toUpperCase().includes(searchText)
    );

    this.#header.value.count = stats.count;
  }

  /**
   * Sync lines array with the actual data source.
   * This function reads all logins from the storage, adds or updates lines and
   * removes lines for the removed logins.
   */
  async #reloadDataSource() {
    this.doneReloadDataSource = false;

    const logins = await LoginHelper.getAllUserFacingLogins();
    const breachesMap = lazy.BREACH_ALERTS_ENABLED
      ? await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins)
      : new Map();
    const vulnerableMap =
      await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
        logins
      );

    this.#syncReloadDataSource(logins, breachesMap, vulnerableMap);

    this.doneReloadDataSource = true;
  }

  /**
   * Implementation between `beforeReloadingDataSource` and `afterReloadingDataSource`
   * should be synchronous because the two functions operates on member variable
   * #linesToForget and they don't expect it to be changed in the middle of reloading.
   */
  #syncReloadDataSource(logins, breachesMap, vulnerableMap) {
    this.beforeReloadingDataSource();

    const loginsWithAlerts = logins.filter(
      login =>
        breachesMap.has(login.guid) ||
        vulnerableMap.has(login.guid) ||
        !login.username.length
    );

    const filteredLogins =
      this.#displayMode === DISPLAY_MODES.ALERTS ? loginsWithAlerts : logins;

    filteredLogins.forEach(login => {
      // Similar domains will be grouped together
      // www. will have least effect on the sorting
      const parts = login.displayOrigin.split(".");

      // Exclude TLD domain
      //todo support eTLD and use public suffix here https://publicsuffix.org
      if (parts.length > 1) {
        parts.length -= 1;
      }
      const isLoginBreached = breachesMap.has(login.guid);
      const isLoginVulnerable = vulnerableMap.has(login.guid);
      const loginNoUsername = !login.username.length;

      let alertValue;
      if (isLoginBreached) {
        alertValue = ALERT_VALUES.breached;
      } else if (isLoginVulnerable) {
        alertValue = ALERT_VALUES.vulnerable;
      } else if (loginNoUsername) {
        alertValue = ALERT_VALUES.noUsername;
      } else {
        alertValue = ALERT_VALUES.none;
      }

      const domain = parts.reverse().join(".");
      const lineId =
        this.#displayMode === DISPLAY_MODES.ALERTS
          ? `${alertValue}:${domain}:${login.username}:${login.guid}`
          : `${domain}:${login.username}:${login.guid}`;

      let originLine = this.addOrUpdateLine(
        login,
        lineId + "0",
        this.#originPrototype
      );
      this.addOrUpdateLine(login, lineId + "1", this.#usernamePrototype);
      let passwordLine = this.addOrUpdateLine(
        login,
        lineId + "2",
        this.#passwordPrototype
      );

      originLine.breached = isLoginBreached;
      passwordLine.vulnerable = isLoginVulnerable;
    });

    this.#header.value.total = logins.length;
    this.#header.value.alerts = loginsWithAlerts.length;
    this.afterReloadingDataSource();
  }

  getAuthTimeoutMs() {
    return AUTH_TIMEOUT_MS;
  }

  observe(_subj, topic, message) {
    if (
      topic == "passwordmgr-storage-changed" ||
      topic == "passwordmgr-crypto-login" ||
      message == "signon.rememberSignons" ||
      message == "signon.management.page.breach-alerts.enabled" ||
      message == "signon.management.page.vulnerable-passwords.enabled"
    ) {
      this.#reloadDataSource();

      if (topic === "passwordmgr-crypto-login") {
        this.setPrimaryPasswordAuthenticated(true);
      }
    } else if (topic == "passwordmgr-crypto-loginCanceled") {
      this.setPrimaryPasswordAuthenticated(false);
    }
  }
}
