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

/**
 * LoginManagerStorage implementation for the Rust logins storage back-end.
 */

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",

  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
});

ChromeUtils.defineLazyGetter(
  lazy,
  "l10n",
  () => new Localization(["toolkit/passwordmgr/passwordmgr.ftl"])
);

import { initialize as initRustComponents } from "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustInitRustComponents.sys.mjs";

import {
  LoginEntry,
  LoginMeta,
  LoginEntryWithMeta,
  BulkResultEntry,
  PrimaryPasswordAuthenticator,
  createLoginStoreWithNssKeymanager,
  AuthenticationCanceled,
} from "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustLogins.sys.mjs";

const LoginInfo = Components.Constructor(
  "@mozilla.org/login-manager/loginInfo;1",
  "nsILoginInfo",
  "init"
);

// Convert a LoginInfo, as known to the JS world, to a LoginEntry, the Rust
// Login type.
// This could be an instance method implemented in
// toolkit/components/passwordmgr/LoginInfo.sys.mjs
// but I'd like to decouple from as many components as possible by now
const loginInfoToLoginEntry = loginInfo =>
  new LoginEntry({
    origin: loginInfo.origin,
    httpRealm: loginInfo.httpRealm,
    formActionOrigin: loginInfo.formActionOrigin,
    usernameField: loginInfo.usernameField,
    passwordField: loginInfo.passwordField,
    username: loginInfo.username,
    password: loginInfo.password,
  });

// Convert a LoginInfo to a LoginEntryWithMeta, to be used for migrating
// records between legacy and Rust storage.
const loginInfoToLoginEntryWithMeta = loginInfo => {
  const now = Date.now();
  return new LoginEntryWithMeta({
    entry: loginInfoToLoginEntry(loginInfo),
    meta: new LoginMeta({
      id: loginInfo.guid || Services.uuid.generateUUID().toString(),
      timesUsed: loginInfo.timesUsed || 1,
      timeCreated: loginInfo.timeCreated || now,
      timeLastUsed: loginInfo.timeLastUsed || now,
      timePasswordChanged: loginInfo.timePasswordChanged || now,
      timeLastBreachAlertDismissed:
        loginInfo.timeLastBreachAlertDismissed || null,
    }),
  });
};

// Convert a Login instance, as returned from Rust Logins, to a LoginInfo
const loginToLoginInfo = login => {
  const loginInfo = new LoginInfo(
    login.origin,
    login.formActionOrigin,
    login.httpRealm,
    login.username,
    login.password,
    login.usernameField,
    login.passwordField
  );

  // add meta information
  loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
  // Rust Login ids are guids
  loginInfo.guid = login.id;
  loginInfo.timeCreated = login.timeCreated;
  loginInfo.timeLastUsed = login.timeLastUsed;
  loginInfo.timePasswordChanged = login.timePasswordChanged;
  loginInfo.timesUsed = login.timesUsed;
  loginInfo.timeLastBreachAlertDismissed =
    login.timeLastBreachAlertDismissed || null;

  /* These fields are not attributes on the Rust Login class
  loginInfo.syncCounter = login.syncCounter;
  loginInfo.everSynced = login.everSynced;
  loginInfo.unknownFields = login.encryptedUnknownFields;
  */

  return loginInfo;
};

// Build a bare LoginInfo carrying only the given guid. Used to report the ids
// of removed logins in "removeAllLogins" notifications, since the bulk deletion
// APIs only return guids rather than full logins.
const guidToLoginInfo = guid => {
  const loginInfo = new LoginInfo("", "", null, "", "", "", "");
  loginInfo.QueryInterface(Ci.nsILoginMetaInfo);
  loginInfo.guid = guid;
  return loginInfo;
};

// An adapter which talks to the Rust Logins Store via LoginInfo objects
class RustLoginsStoreAdapter {
  #store = null;

  constructor(store) {
    this.#store = store;
  }

  async get(id) {
    const login = await this.#store.get(id);
    return login && loginToLoginInfo(login);
  }

  async list() {
    const logins = await this.#store.list();
    return logins.map(loginToLoginInfo);
  }

  async update(id, loginInfo) {
    const loginEntry = loginInfoToLoginEntry(loginInfo);
    const login = await this.#store.update(id, loginEntry);
    return loginToLoginInfo(login);
  }

  async add(loginInfo) {
    const loginEntry = loginInfoToLoginEntry(loginInfo);
    const login = await this.#store.add(loginEntry);
    return loginToLoginInfo(login);
  }

  async addWithMeta(loginInfo) {
    const loginEntryWithMeta = loginInfoToLoginEntryWithMeta(loginInfo);
    const login = await this.#store.addWithMeta(loginEntryWithMeta);
    return loginToLoginInfo(login);
  }

  async addManyWithMeta(loginInfos, continueOnDuplicates) {
    const loginEntriesWithMeta = loginInfos.map(loginInfoToLoginEntryWithMeta);
    const results = await this.#store.addManyWithMeta(loginEntriesWithMeta);

    // on continuous mode, return result objects, which could be either a login
    // or an error containing the error message
    if (continueOnDuplicates) {
      return results.map(l => {
        if (l instanceof BulkResultEntry.Error) {
          return {
            error: { message: l.message },
          };
        }
        return {
          login: loginToLoginInfo(l.login),
        };
      });
    }

    // otherwise throw first error
    const error = results.find(l => l instanceof BulkResultEntry.Error);
    if (error) {
      throw new Error(error.message);
    }
    // and return login info objects
    return results
      .filter(l => l instanceof BulkResultEntry.Success)
      .map(({ login }) => loginToLoginInfo(login));
  }

  async delete(id) {
    return await this.#store.delete(id);
  }

  async deleteMany(ids) {
    return await this.#store.deleteMany(ids);
  }

  async deleteAll() {
    return await this.#store.deleteAll();
  }

  async deleteAllExceptFxa() {
    return await this.#store.deleteAllExceptFxa();
  }

  // reset() {
  //   return this.#store.reset()
  // }

  async wipeLocal() {
    return await this.#store.wipeLocal();
  }

  async wipeLocalExceptFxa() {
    return await this.#store.wipeLocalExceptFxa();
  }

  async count() {
    return await this.#store.count();
  }

  async touch(id) {
    return await this.#store.touch(id);
  }

  async findLoginToUpdate(loginInfo) {
    const loginEntry = loginInfoToLoginEntry(loginInfo);
    const login = await this.#store.findLoginToUpdate(loginEntry);
    return login && loginToLoginInfo(login);
  }

  async recordPotentiallyVulnerablePasswords(passwords) {
    await this.#store.recordPotentiallyVulnerablePasswords(passwords);
  }

  async isPotentiallyVulnerablePassword(id) {
    return this.#store.isPotentiallyVulnerablePassword(id);
  }

  async recordBreachAlertDismissal(id) {
    await this.#store.recordBreachAlertDismissal(id);
  }

  async clearAllPotentiallyVulnerablePasswords() {
    await this.#store.resetAllBreaches();
  }

  async arePotentiallyVulnerablePasswords(ids) {
    return this.#store.arePotentiallyVulnerablePasswords(ids);
  }

  async getBreachAlertDismissalsByLoginGUID() {
    const result = {};
    for (const {
      id,
      timeLastBreachAlertDismissed: timeBreachAlertDismissed,
    } of await this.#store.list()) {
      if (timeBreachAlertDismissed) {
        result[id] = {
          timeBreachAlertDismissed,
        };
      }
    }
    return result;
  }

  bridgedEngine() {
    return this.#store.bridgedEngine();
  }

  shutdown() {
    return this.#store.shutdown();
  }
}

class RustLoginStorageAuthenticator extends PrimaryPasswordAuthenticator {
  #logger = null;

  // True while a primary-password prompt is displayed, so the storage can
  // report `uiBusy` like the SDR crypto does.
  uiBusy = false;

  // Set when the user cancels the prompt. The storage uses this to translate
  // the resulting store failure into NS_ERROR_ABORT (matching crypto-SDR).
  authCanceled = false;

  constructor() {
    super();
    this.#logger = lazy.LoginHelper.createLogger(
      "RustLoginStorageAuthenticator"
    );
  }

  // Called by Rust when the NSS key needs to be unlocked. Concurrent calls are not
  // possible: all store operations hold the store's internal Mutex<LoginDb> while
  // calling get_key(), so this method is always invoked serially.
  async getPrimaryPassword() {
    this.#logger.log("getPrimaryPassword called");
    this.authCanceled = false;
    this.uiBusy = true;
    try {
      const win = Services.wm.getMostRecentBrowserWindow();
      // Empty title causes Prompter.sys.mjs to fall back to the localised
      // "PromptPassword3" string ("Password Required - <AppName>").
      const message = await lazy.l10n.formatValue(
        "primary-password-prompt-message"
      );
      const result = await Services.prompt.asyncPromptPassword(
        win?.browsingContext,
        Services.prompt.MODAL_TYPE_WINDOW,
        "",
        message,
        ""
      );

      if (!result.getProperty("ok")) {
        this.authCanceled = true;
        Services.obs.notifyObservers(null, "passwordmgr-crypto-loginCanceled");
        throw new AuthenticationCanceled("User cancelled");
      }

      this.#logger.log("got a password");
      return result.getProperty("pass");
    } finally {
      this.uiBusy = false;
    }
  }

  async onAuthenticationSuccess() {
    Services.obs.notifyObservers(null, "passwordmgr-crypto-login");
    this.#logger.log("authenticated with success");
  }

  async onAuthenticationFailure() {
    this.#logger.log("failed to authenticate");
  }
}

export class LoginManagerRustStorage {
  #storageAdapter = null;
  #authenticator = null;
  #initializationPromise = null;
  // Only the active backend fires storage-changed events to avoid duplicates
  // when both JSON and Rust stores are initialized.
  // Default is false (json is active)
  #isActive = false;
  set isActive(v) {
    this.#isActive = v;
  }

  // have it a singleton
  constructor() {
    if (LoginManagerRustStorage._instance) {
      return LoginManagerRustStorage._instance;
    }
    this.__crypto = null; // nsILoginManagerCrypto service
    LoginManagerRustStorage._instance = this;
  }

  initialize() {
    if (this.#initializationPromise) {
      this.log("rust storage already initialized");
    } else {
      try {
        const profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
        const path = `${profilePath}/logins.db`;

        this.#initializationPromise = new Promise(resolve => {
          this.log(`Initializing Rust login storage at ${path}`);

          initRustComponents(profilePath).then(() => {
            const authenticator = new RustLoginStorageAuthenticator();
            this.#authenticator = authenticator;
            const store = createLoginStoreWithNssKeymanager(
              path,
              authenticator
            );

            this.#storageAdapter = new RustLoginsStoreAdapter(store);
            this.log("Rust login storage ready.");

            this._registerShutdownBlocker().then(() => resolve(this));
          });
        });
      } catch (e) {
        this.log(`Initialization failed ${e.name}.`);
        this.log(e);
        throw new Error("Initialization failed");
      }
    }

    return this.#initializationPromise;
  }

  /**
   * Terminate all pending writes. After this call, the store can't be used.
   */
  async finalize() {
    // TODO: Currently we do not mark the instance as closed, not sure if later
    // calls would be rejected elsewhere.

    await this.#storageAdapter.shutdown();
  }

  /**
   * Ensure the storage is finalized at shutdown. All LoginManager storage
   * backends must have their own shutdown blocker to finalize properly.
   *
   * In the corner case where the shutdown phase has already passed by the time
   * we get here, registering a blocker would throw, so we call `finalize()`
   * immediately instead.
   *
   * @param {object} phase An `AsyncShutdown` phase object. Exposed as a
   *   parameter for testing.
   */
  _registerShutdownBlocker(phase = lazy.AsyncShutdown.profileChangeTeardown) {
    if (phase.isClosed) {
      return this.finalize();
    }
    phase.addBlocker(
      "LoginManagerRustStorage: Interrupt IO operations on login store",
      async () => this.finalize()
    );
    return Promise.resolve();
  }

  /**
   * Internal method used by tests only. It is called before replacing
   * this storage module with a new instance.
   */
  testSaveForReplace() {}

  /**
   * Returns the Sync bridged engine backed by the same Rust store instance the
   * storage layer uses, so that Sync and data access share one database handle.
   * Assumes the storage is already initialized; the sync engine only reaches it
   * via the active store, which is set after initialization completes.
   *
   */
  bridgedEngine() {
    return this.#storageAdapter.bridgedEngine();
  }

  /**
   * Returns the "sync id" used by Sync to know whether the store is current with
   * respect to the sync servers. It is stored encrypted, but only so we
   * can detect failure to decrypt (for example, a "reset" of the primary
   * password will leave all logins alone, but they will fail to decrypt. We
   * also want this metadata to be unavailable in that scenario)
   *
   * Returns null if the data doesn't exist or if the data can't be
   * decrypted (including if the primary-password prompt is cancelled). This is
   * OK for Sync as it can't even begin syncing if the primary-password is
   * locked as the sync encrytion keys are stored in this login manager.
   */
  async getSyncID() {
    throw Components.Exception("getSyncID", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  async setSyncID(_syncID) {
    throw Components.Exception("setSyncID", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  async getLastSync() {
    throw Components.Exception("getLastSync", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  async setLastSync(_timestamp) {
    throw Components.Exception("setLastSync", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  loginIsDeleted(_guid) {
    throw Components.Exception("loginIsDeleted", Cr.NS_ERROR_NOT_IMPLEMENTED);
  }

  loginIsDeletedAsync(_guid) {
    throw Components.Exception(
      "loginIsDeletedAsync",
      Cr.NS_ERROR_NOT_IMPLEMENTED
    );
  }

  async addLoginsAsync(logins, continueOnDuplicates = false) {
    const result = await this.#addLoginsWithResults(
      logins,
      continueOnDuplicates
    );

    // Return only successfully added logins, matching the JSON backend.
    return continueOnDuplicates
      ? result.filter(item => !item.error).map(item => item.login)
      : result;
  }

  /**
   * Adds multiple logins and returns the raw per-login results, with each
   * entry being either { login } or { error } and aligned by index with the
   * input. Used by the migrator to detect and rescue duplicates; most callers
   * want addLoginsAsync instead.
   */
  async addLoginsWithResultsAsync(logins) {
    return this.#addLoginsWithResults(logins, true);
  }

  async #addLoginsWithResults(logins, continueOnDuplicates) {
    if (logins.length === 0) {
      return logins;
    }

    // The Rust backend only treats an exact formActionOrigin match as a
    // duplicate, whereas desktop treats an empty formActionOrigin as a wildcard
    // (see LoginHelper.doLoginsMatch). Reproduce the JSON backend's check up
    // front so a manually-added login (formActionOrigin === "") is rejected as
    // a duplicate, and so callers such as about:logins get the existing login's
    // GUID to link to and select the conflicting login.
    if (!continueOnDuplicates) {
      const existing = await this.#findExistingDuplicate(logins);
      if (existing) {
        throw lazy.LoginHelper.createLoginAlreadyExistsError(existing.guid);
      }
    }

    const result = await this.#storageAdapter.addManyWithMeta(
      logins,
      continueOnDuplicates
    );

    if (this.#isActive) {
      Glean.pwmgr.numSavedPasswords.set(
        await this.countLoginsAsync("", "", "")
      );
      for (const item of result) {
        const login = continueOnDuplicates ? item.login : item;
        if (login) {
          lazy.LoginHelper.notifyStorageChanged("addLogin", login);
        }
      }
    }

    return result;
  }

  // Finds the stored login that a to-be-added login duplicates, so the existing
  // login's GUID can be reported in the "already exists" error. Mirrors the
  // duplicate lookup in modifyLoginAsync.
  async #findExistingDuplicate(logins) {
    for (const login of logins) {
      const matchData = {};
      for (const field of ["origin", "formActionOrigin", "httpRealm"]) {
        if (login[field] != "") {
          matchData[field] = login[field];
        }
      }
      const [matches] = await this.#searchLogins(matchData);
      const match = matches.find(l => login.matches(l, true));
      if (match) {
        return match;
      }
    }
    return null;
  }

  async modifyLoginAsync(oldLogin, newLoginData, _fromSync) {
    // Resolve the stored login by value so callers may pass a login without a
    // guid, matching the JSON storage backend.
    const oldStoredLogin =
      await this.#storageAdapter.findLoginToUpdate(oldLogin);

    if (!oldStoredLogin) {
      throw new Error("No matching logins");
    }

    const idToModify = oldStoredLogin.guid;

    const newLogin = lazy.LoginHelper.buildModifiedLogin(
      oldStoredLogin,
      newLoginData
    );

    // Check if the new GUID is duplicate.
    if (
      newLogin.guid != idToModify &&
      (await this.#storageAdapter.get(newLogin.guid))
    ) {
      throw new Error("specified GUID already exists");
    }

    // Look for an existing entry in case key properties changed.
    if (!newLogin.matches(oldLogin, true)) {
      const matchData = {};
      for (const field of ["origin", "formActionOrigin", "httpRealm"]) {
        if (newLogin[field] != "") {
          matchData[field] = newLogin[field];
        }
      }
      const [logins] = await this.#searchLogins(matchData);

      const matchingLogin = logins.find(login => newLogin.matches(login, true));
      if (matchingLogin) {
        throw lazy.LoginHelper.createLoginAlreadyExistsError(
          matchingLogin.guid
        );
      }
    }

    const updatedLogin = await this.#storageAdapter.update(
      idToModify,
      newLogin
    );

    // The Rust `update` deliberately treats an edit as "not a use" and leaves
    // timesUsed/timeLastUsed unchanged (bug 2045032). Honor a requested
    // increment (e.g. timesUsedIncrement) by recording the use(s) via `touch`.
    let finalLogin = updatedLogin;
    const increment = newLogin.timesUsed - oldStoredLogin.timesUsed;
    for (let i = 0; i < increment; i++) {
      await this.#storageAdapter.touch(updatedLogin.guid);
    }
    if (increment > 0) {
      finalLogin = await this.#storageAdapter.get(updatedLogin.guid);
    }

    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged("modifyLogin", [
        oldStoredLogin,
        finalLogin,
      ]);
    }

    return finalLogin;
  }

  async recordPasswordUseAsync(login) {
    const oldStoredLogin = await this.#storageAdapter.findLoginToUpdate(login);

    if (!oldStoredLogin) {
      throw new Error("No matching logins");
    }

    await this.#storageAdapter.touch(oldStoredLogin.guid);
    const updatedLogin = await this.#storageAdapter.get(oldStoredLogin.guid);

    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged("modifyLogin", [
        oldStoredLogin,
        updatedLogin,
      ]);
    }
  }

  async recordBreachAlertDismissal(loginGUID) {
    await this.#storageAdapter.recordBreachAlertDismissal(loginGUID);
  }

  async getBreachAlertDismissalsByLoginGUID() {
    return this.#storageAdapter.getBreachAlertDismissalsByLoginGUID();
  }

  /**
   * Returns an array of nsILoginInfo. If decryption of a login
   * fails due to a corrupt entry, the login is not included in
   * the resulting array.
   *
   * @returns {Promise<nsILoginInfo[]>}
   */
  async getAllLogins(includeDeleted) {
    // `includeDeleted` is currentlty unsupported
    if (includeDeleted) {
      throw Components.Exception(
        "getAllLogins with includeDeleted",
        Cr.NS_ERROR_NOT_IMPLEMENTED
      );
    }
    try {
      return await this.#storageAdapter.list();
    } catch (e) {
      // The store fails to decrypt when the primary password is locked (either
      // the user cancelled the prompt, or the key can't be unlocked). Translate
      // that into NS_ERROR_ABORT so callers (e.g. getAllUserFacingLogins) treat
      // it as "no logins available right now" instead of leaking a raw Rust
      // error, matching crypto-SDR and searchLoginsAsync.
      if (
        this.#authenticator?.authCanceled ||
        /decryption failed/i.test(e.message)
      ) {
        if (this.#authenticator) {
          this.#authenticator.authCanceled = false;
        }
        throw Components.Exception(
          "Primary password locked",
          Cr.NS_ERROR_ABORT
        );
      }
      throw e;
    }
  }

  async searchLoginsAsync(matchData, includeDeleted) {
    const realMatchData = {};
    const options = {};

    if ("guid" in matchData) {
      realMatchData.guid = matchData.guid;
    } else {
      for (const name in matchData) {
        switch (name) {
          // Some property names aren't field names but are special options to
          // affect the search.
          case "acceptDifferentSubdomains":
          case "schemeUpgrades":
          case "acceptRelatedRealms":
          case "relatedRealms": {
            options[name] = matchData[name];
            break;
          }
          default: {
            realMatchData[name] = matchData[name];
            break;
          }
        }
      }
    }

    try {
      const [logins] = await this.#searchLogins(
        realMatchData,
        includeDeleted,
        options
      );
      return logins;
    } catch (e) {
      // When the user cancels the primary-password prompt, the store fails to
      // decrypt. Translate that into NS_ERROR_ABORT so callers (e.g.
      // LoginManagerParent) can throttle further prompts, matching crypto-SDR.
      if (this.#authenticator?.authCanceled) {
        this.#authenticator.authCanceled = false;
        throw Components.Exception(
          "User canceled primary password entry",
          Cr.NS_ERROR_ABORT
        );
      }
      throw e;
    }
  }

  async #searchLogins(
    matchData,
    includeDeleted = false,
    aOptions = {
      schemeUpgrades: false,
      acceptDifferentSubdomains: false,
      acceptRelatedRealms: false,
      relatedRealms: [],
    },
    candidateLogins
  ) {
    candidateLogins ||= await this.#storageAdapter.list();

    function match(aLoginItem) {
      for (const field in matchData) {
        const wantedValue = matchData[field];

        // Override the storage field name for some fields due to backwards
        // compatibility with Sync/storage.
        let storageFieldName = field;
        switch (field) {
          case "formActionOrigin": {
            storageFieldName = "formSubmitURL";
            break;
          }
          case "origin": {
            storageFieldName = "hostname";
            break;
          }
        }

        switch (field) {
          case "formActionOrigin":
            if (wantedValue != null) {
              // Historical compatibility requires this special case
              if (
                aLoginItem.formSubmitURL == "" ||
                (wantedValue == "" && Object.keys(matchData).length != 1)
              ) {
                break;
              }
              if (
                !lazy.LoginHelper.isOriginMatching(
                  aLoginItem[storageFieldName],
                  wantedValue,
                  aOptions
                )
              ) {
                return false;
              }
              break;
            }
          // fall through
          case "origin":
            if (wantedValue != null) {
              // needed for formActionOrigin fall through
              if (
                !lazy.LoginHelper.isOriginMatching(
                  aLoginItem[storageFieldName],
                  wantedValue,
                  aOptions
                )
              ) {
                return false;
              }
              break;
            }
          // Normal cases.
          // fall through
          case "httpRealm":
          case "id":
          case "usernameField":
          case "passwordField":
          case "encryptedUsername":
          case "encryptedPassword":
          case "guid":
          case "encType":
          case "timeCreated":
          case "timeLastUsed":
          case "timePasswordChanged":
          case "timesUsed":
          case "syncCounter":
          case "everSynced":
            if (wantedValue == null && aLoginItem[storageFieldName]) {
              return false;
            } else if (aLoginItem[storageFieldName] != wantedValue) {
              return false;
            }
            break;
          // Fail if caller requests an unknown property.
          default:
            throw new Error("Unexpected field: " + field);
        }
      }
      return true;
    }

    const foundLogins = [];
    const foundIds = [];

    for (const login of candidateLogins) {
      if (login.deleted && !includeDeleted) {
        continue; // skip deleted items
      }

      if (match(login)) {
        foundLogins.push(login);
        foundIds.push(login.guid);
      }
    }

    this.log(
      `Returning ${foundLogins.length} logins for specified origin with options ${aOptions}`
    );
    return [foundLogins, foundIds];
  }

  async removeLoginAsync(login, _fromSync) {
    // Resolve the stored login by value so callers may pass a login without a
    // guid, matching the JSON storage backend.
    const storedLogin = await this.#storageAdapter.findLoginToUpdate(login);
    if (!storedLogin) {
      throw new Error("No matching logins");
    }

    await this.#storageAdapter.delete(storedLogin.guid);

    if (this.#isActive) {
      Glean.pwmgr.numSavedPasswords.set(
        await this.countLoginsAsync("", "", "")
      );
      lazy.LoginHelper.notifyStorageChanged("removeLogin", storedLogin);
    }
  }

  /**
   * Removes all logins from local storage, including the FxA Sync key. The
   * logins are marked as deleted so the deletions propagate to Sync.
   *
   * NOTE: You probably want removeAllUserFacingLoginsAsync instead of this
   * function.
   */
  async removeAllLoginsAsync() {
    this.log("Removing all logins.");
    const removedIds = await this.#storageAdapter.deleteAll();

    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged(
        "removeAllLogins",
        removedIds.map(guidToLoginInfo)
      );
    }
  }

  /**
   * Removes all user facing logins from storage, i.e. all logins except the
   * FxA Sync key.
   *
   * If you need to remove the FxA key, use `removeAllLoginsAsync` instead.
   *
   * @param fullyRemove remove the logins rather than mark them deleted.
   */
  async removeAllUserFacingLoginsAsync(fullyRemove) {
    this.log("Removing all user facing logins.");
    let removedIds = [];
    if (fullyRemove) {
      // wipeLocalExceptFxa() does not return the removed ids.
      await this.#storageAdapter.wipeLocalExceptFxa();
    } else {
      removedIds = await this.#storageAdapter.deleteAllExceptFxa();
    }

    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged(
        "removeAllLogins",
        removedIds.map(guidToLoginInfo)
      );
    }
  }

  async countLoginsAsync(origin, formActionOrigin, httpRealm) {
    // The optimized adapter path only applies when all fields are the empty
    // string wildcard. Origin and formActionOrigin must go through the generic
    // search below so that count and search share the same strict matching
    // semantics; the native countByOrigin/countByFormActionOrigin paths
    // normalize the origin (e.g. stripping a trailing slash) and would count
    // logins that searchLoginsAsync does not return.
    if (origin === "" && formActionOrigin === "" && httpRealm === "") {
      return await this.#storageAdapter.count();
    }

    const loginData = {
      origin,
      formActionOrigin,
      httpRealm,
    };

    const matchData = {};
    for (const field of ["origin", "formActionOrigin", "httpRealm"]) {
      if (loginData[field] != "") {
        matchData[field] = loginData[field];
      }
    }
    const [logins] = await this.#searchLogins(matchData);

    this.log(`Counted ${logins.length} logins.`);
    return logins.length;
  }

  async addPotentiallyVulnerablePassword(login) {
    await this.#storageAdapter.recordPotentiallyVulnerablePasswords([
      login.password,
    ]);
    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged(
        "addPotentiallyVulnerablePassword",
        login
      );
    }
  }

  // adding multiple potentially vulnerable passwords during migration
  async addPotentiallyVulnerablePasswords(passwords) {
    await this.#storageAdapter.recordPotentiallyVulnerablePasswords(passwords);
  }

  async isPotentiallyVulnerablePassword(login) {
    return this.#storageAdapter.isPotentiallyVulnerablePassword(
      login.QueryInterface(Ci.nsILoginMetaInfo).guid
    );
  }

  async arePotentiallyVulnerablePasswords(logins) {
    // Logins without a guid aren't stored, so they can't be recorded as
    // vulnerable. Filter them out to avoid passing null guids to the Rust API.
    const ids = logins
      .map(
        l =>
          (typeof l.QueryInterface === "function"
            ? l.QueryInterface(Ci.nsILoginMetaInfo)
            : l
          ).guid
      )
      .filter(Boolean);
    return this.#storageAdapter.arePotentiallyVulnerablePasswords(ids);
  }

  async clearAllPotentiallyVulnerablePasswords() {
    await this.#storageAdapter.clearAllPotentiallyVulnerablePasswords();
    if (this.#isActive) {
      lazy.LoginHelper.notifyStorageChanged(
        "clearAllPotentiallyVulnerablePasswords"
      );
    }
  }

  get _crypto() {
    if (!this.__crypto) {
      this.__crypto = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
        Ci.nsILoginManagerCrypto
      );
    }
    return this.__crypto;
  }

  get uiBusy() {
    // The Rust store shows its own primary-password prompt via the
    // authenticator, which the SDR crypto is unaware of.
    return this.#authenticator?.uiBusy || this._crypto.uiBusy;
  }

  get isLoggedIn() {
    return this._crypto.isLoggedIn;
  }
}

ChromeUtils.defineLazyGetter(LoginManagerRustStorage.prototype, "log", () => {
  const logger = lazy.LoginHelper.createLogger("RustLogins");
  return logger.log.bind(logger);
});
