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

/**
 * Handles serialization of the data and persistence into a file.
 *
 * The file is stored in JSON format, without indentation, using UTF-8 encoding.
 * With indentation applied, the file would look like this:
 *
 * {
 *   "logins": [
 *     {
 *       "id": 2,
 *       "hostname": "http://www.example.com",
 *       "httpRealm": null,
 *       "formSubmitURL": "http://www.example.com",
 *       "usernameField": "username_field",
 *       "passwordField": "password_field",
 *       "encryptedUsername": "...",
 *       "encryptedPassword": "...",
 *       "guid": "...",
 *       "encType": 1,
 *       "timeCreated": 1262304000000,
 *       "timeLastUsed": 1262304000000,
 *       "timePasswordChanged": 1262476800000,
 *       "timesUsed": 1
 *        // only present if other clients had fields we didn't know about
 *       "encryptedUnknownFields: "...",
 *     },
 *     {
 *       "id": 4,
 *       (...)
 *     }
 *   ],
 *   "nextId": 10,
 *   "version": 1
 * }
 */

// Globals

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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.sys.mjs",
  FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.sys.mjs",
});

/**
 * Current data version assigned by the code that last touched the data.
 *
 * This number should be updated only when it is important to understand whether
 * an old version of the code has touched the data, for example to execute an
 * update logic.  In most cases, this number should not be changed, in
 * particular when no special one-time update logic is needed.
 *
 * For example, this number should NOT be changed when a new optional field is
 * added to a login entry.
 */
const kDataVersion = 3;

const MAX_DATE_MS = 8640000000000000;

// LoginStore

/**
 * Inherits from JSONFile and handles serialization of login-related data and
 * persistence into a file.
 *
 * @param aPath
 *        String containing the file path where data should be saved.
 */
export function LoginStore(aPath, aBackupPath = "") {
  JSONFile.call(this, {
    path: aPath,
    dataPostProcessor: this._dataPostProcessor.bind(this),
    backupTo: aBackupPath,
  });
}

LoginStore.prototype = Object.create(JSONFile.prototype);
LoginStore.prototype.constructor = LoginStore;

LoginStore.prototype._save = async function () {
  await JSONFile.prototype._save.call(this);
  // Notify tests that writes to the login store is complete.
  Services.obs.notifyObservers(null, "password-storage-updated");

  if (this._options.backupTo) {
    await this._backupHandler();
  }
};

/**
 * Delete logins backup file if the last saved login was removed using
 * removeLogin() or if all logins were removed at once using removeAllUserFacingLogins().
 * Note that if the user has a fxa key stored as a login, we just update the
 * backup to only store the key when the last saved user facing login is removed.
 */
LoginStore.prototype._backupHandler = async function () {
  const logins = this._data.logins;
  // Return early if more than one login is stored because the backup won't need
  // updating in this case.
  if (logins.length > 1) {
    return;
  }

  // If one login is stored and it's a fxa sync key, we update the backup to store the
  // key only.
  if (
    logins.length &&
    logins[0].hostname == lazy.FXA_PWDMGR_HOST &&
    logins[0].httpRealm == lazy.FXA_PWDMGR_REALM
  ) {
    try {
      await IOUtils.copy(this.path, this._options.backupTo);

      // This notification is specifically sent out for a test.
      Services.obs.notifyObservers(null, "logins-backup-updated");
    } catch (ex) {
      console.error(ex);
    }
  } else if (!logins.length) {
    // If no logins are stored anymore, delete backup.
    await IOUtils.remove(this._options.backupTo, {
      ignoreAbsent: true,
    });
  }
};

/**
 * Synchronously work on the data just loaded into memory.
 */
LoginStore.prototype._dataPostProcessor = function (data) {
  if (data.nextId === undefined) {
    data.nextId = 1;
  }

  // Create any arrays that are not present in the saved file.
  if (!data.logins) {
    data.logins = [];
  }

  if (!data.potentiallyVulnerablePasswords) {
    data.potentiallyVulnerablePasswords = [];
  }

  // Migrate breach alert dismissals from the separate map into per-login fields.
  if (data.dismissedBreachAlertsByLoginGUID) {
    for (let login of data.logins) {
      if (login.guid in data.dismissedBreachAlertsByLoginGUID) {
        const { timeBreachAlertDismissed } =
          data.dismissedBreachAlertsByLoginGUID[login.guid];
        login.timeLastBreachAlertDismissed = timeBreachAlertDismissed;
      }
    }
    delete data.dismissedBreachAlertsByLoginGUID;
  }

  // sanitize dates in logins
  if (!("version" in data) || data.version < 3) {
    let dateProperties = ["timeCreated", "timeLastUsed", "timePasswordChanged"];
    let now = Date.now();
    function getEarliestDate(login, defaultDate) {
      let earliestDate = dateProperties.reduce((earliest, pname) => {
        let ts = login[pname];
        return !ts ? earliest : Math.min(ts, earliest);
      }, defaultDate);
      return earliestDate;
    }
    for (let login of data.logins) {
      for (let pname of dateProperties) {
        let earliestDate;
        if (!login[pname] || login[pname] > MAX_DATE_MS) {
          login[pname] =
            earliestDate || (earliestDate = getEarliestDate(login, now));
        }
      }
    }
  }

  // Indicate that the current version of the code has touched the file.
  data.version = kDataVersion;

  return data;
};
