/* 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 https://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
  SearchUtils: "moz-src:///toolkit/components/search/SearchUtils.sys.mjs",
  SelectableProfileService:
    "resource:///modules/profiles/SelectableProfileService.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
  return console.createInstance({
    prefix: "PreferencesBackupResource",
    maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false)
      ? "Debug"
      : "Warn",
  });
});

const PROFILE_RESTORATION_DATE_PREF = "browser.backup.profile-restoration-date";

/**
 * Class representing files that modify preferences and permissions within a user profile.
 */
export class PreferencesBackupResource extends BackupResource {
  static get key() {
    return "preferences";
  }

  static get requiresEncryption() {
    return false;
  }

  /**
   * Adds prefs to the override map that are currently set but should not be
   * included in the backup.  Override them with null values to prevent
   * serialization.
   *
   * @param {nsIPrefOverrideMap} prefsOverrideMap
   * @returns {nsIPrefOverrideMap} prefsOverrideMap with ignored prefs added
   */
  static addPrefsToIgnoreInBackup(prefsOverrideMap) {
    // List of prefs we never backup.
    let kIgnoredPrefs = [
      "app.normandy.user_id",
      "toolkit.telemetry.cachedClientID",
      "toolkit.telemetry.cachedProfileGroupID",
      PROFILE_RESTORATION_DATE_PREF,
    ];

    const backupPrefs = Services.prefs.getChildList("browser.backup.");
    kIgnoredPrefs = kIgnoredPrefs.concat(backupPrefs);

    // Prefs with this prefix are always overriden.
    const kNimbusMetadataPrefPrefix = "nimbus.";

    for (const pref of kIgnoredPrefs) {
      if (Services.prefs.getPrefType(pref) !== Services.prefs.PREF_INVALID) {
        prefsOverrideMap.addEntry(pref, null);
      }
    }

    const nimbusPrefs = Services.prefs.getChildList(kNimbusMetadataPrefPrefix);
    for (const pref of nimbusPrefs) {
      prefsOverrideMap.addEntry(pref, null);
    }

    return prefsOverrideMap;
  }

  /**
   * Parses preferences from a prefs.js file buffer.
   *
   * @param {Uint8Array} prefsBuffer - The raw bytes of a prefs.js file.
   * @param {string[]} [prefNames] - Optional list of pref names to extract.
   *                                 If not provided, returns all prefs.
   * @returns {Map<string, *>} Map of pref names to their values.
   */
  static getPrefsFromBuffer(prefsBuffer, prefNames = null) {
    const prefSet = prefNames ? new Set(prefNames) : null;
    const prefs = new Map();

    const addPref = (_kind, name, value) => {
      if (!prefSet || prefSet.has(name)) {
        prefs.set(name, value);
      }
    };

    Services.prefs.parsePrefsFromBuffer(prefsBuffer, {
      onStringPref: addPref,
      onIntPref: addPref,
      onBoolPref: addPref,
      onError(_message) {
        // ignore any errors here, we'll use the default value when evaluating
      },
    });

    return prefs;
  }

  async backup(
    stagingPath,
    profilePath = PathUtils.profileDir,
    _isEncrypting = false
  ) {
    // These are files that can be simply copied into the staging folder using
    // IOUtils.copy.
    const simpleCopyFiles = [
      "xulstore.json",
      "containers.json",
      "customKeys.json",
      "handlers.json",
      "search.json.mozlz4",
      "user.js",
      "chrome",
    ];
    await BackupResource.copyFiles(profilePath, stagingPath, simpleCopyFiles);

    // prefs.js is a special case - we have a helper function to flush the
    // current prefs state to disk off of the main thread.
    let prefsDestPath = PathUtils.join(stagingPath, "prefs.js");
    let prefsDestFile = await IOUtils.getFile(prefsDestPath);
    await lazy.ExperimentAPI._rsLoader.withUpdateLock(async () => {
      await Services.prefs.backupPrefFile(
        prefsDestFile,
        PreferencesBackupResource.addPrefsToIgnoreInBackup(
          lazy.ExperimentAPI.manager.store.getOriginalPrefValuesForAllActiveEnrollments()
        )
      );
    });

    // During recovery, we need to recompute verification hashes for any
    // custom engines, but only for engines that were originally passing
    // verification. We'll store the profile path at backup time in our
    // ManifestEntry so that we can do that verification check at recover-time.
    return { profilePath };
  }

  async recover(manifestEntry, recoveryPath, destProfilePath) {
    const SEARCH_PREF_FILENAME = "search.json.mozlz4";
    const RECOVERY_SEARCH_PREF_PATH = PathUtils.join(
      recoveryPath,
      SEARCH_PREF_FILENAME
    );

    if (await IOUtils.exists(RECOVERY_SEARCH_PREF_PATH)) {
      // search.json.mozlz4 may contain hash values that need to be recomputed
      // now that the profile directory has changed.
      let searchPrefs = await IOUtils.readJSON(RECOVERY_SEARCH_PREF_PATH, {
        decompress: true,
      });

      // ... but we only want to do this for engines that had valid verification
      // hashes for the original profile path.
      const ORIGINAL_PROFILE_PATH = manifestEntry.profilePath;

      if (ORIGINAL_PROFILE_PATH) {
        searchPrefs.engines = searchPrefs.engines.map(engine => {
          if (engine._metaData.loadPathHash) {
            let loadPath = engine._loadPath;
            if (
              engine._metaData.loadPathHash ==
              lazy.SearchUtils.getVerificationHash(
                loadPath,
                ORIGINAL_PROFILE_PATH
              )
            ) {
              engine._metaData.loadPathHash =
                lazy.SearchUtils.getVerificationHash(loadPath, destProfilePath);
            }
          }
          return engine;
        });

        if (
          searchPrefs.metaData.defaultEngineIdHash &&
          searchPrefs.metaData.defaultEngineIdHash ==
            lazy.SearchUtils.getVerificationHash(
              searchPrefs.metaData.defaultEngineId,
              ORIGINAL_PROFILE_PATH
            )
        ) {
          searchPrefs.metaData.defaultEngineIdHash =
            lazy.SearchUtils.getVerificationHash(
              searchPrefs.metaData.defaultEngineId,
              destProfilePath
            );
        }

        if (
          searchPrefs.metaData.privateDefaultEngineIdHash &&
          searchPrefs.metaData.privateDefaultEngineIdHash ==
            lazy.SearchUtils.getVerificationHash(
              searchPrefs.metaData.privateDefaultEngineId,
              ORIGINAL_PROFILE_PATH
            )
        ) {
          searchPrefs.metaData.privateDefaultEngineIdHash =
            lazy.SearchUtils.getVerificationHash(
              searchPrefs.metaData.privateDefaultEngineId,
              destProfilePath
            );
        }
      }

      await IOUtils.writeJSON(
        PathUtils.join(destProfilePath, SEARCH_PREF_FILENAME),
        searchPrefs,
        { compress: true }
      );
    }

    const simpleCopyFiles = [
      "prefs.js",
      "xulstore.json",
      "containers.json",
      "customKeys.json",
      "handlers.json",
      "user.js",
      "chrome",
    ];
    await BackupResource.copyFiles(
      recoveryPath,
      destProfilePath,
      simpleCopyFiles
    );

    const LINEBREAK = AppConstants.platform === "win" ? "\r\n" : "\n";
    let prefsFile = await IOUtils.getFile(destProfilePath);
    prefsFile.append("prefs.js");
    // We should always have recovered a prefs.js but, if we didn't for any
    // reason, we can still write the timestamp.  Since we are creating the
    // prefs.js file, we need to add the preamble.
    const includePreamble = !(await IOUtils.exists(prefsFile.path));
    let addToPrefsJs = includePreamble ? Services.prefs.prefsJsPreamble : "";

    // Append browser.backup.scheduled.last-backup-file to prefs.js with the
    // current timestamp.
    addToPrefsJs += `user_pref("${PROFILE_RESTORATION_DATE_PREF}", ${Math.round(Date.now() / 1000)});${LINEBREAK}`;

    await IOUtils.writeUTF8(prefsFile.path, addToPrefsJs, {
      mode: "appendOrCreate",
    });

    if (lazy.SelectableProfileService.currentProfile) {
      lazy.logConsole.debug(
        `We're recovering into a profile group, let's make sure to set the right selectable profile prefs`
      );

      // Before adding prefs to the db, let's make sure we choose the most restrictive settings
      // for data collection in the group.
      const dataCollectionPrefs = [
        "browser.discovery.enabled",
        "app.shield.optoutstudies.enabled",
        "datareporting.healthreport.uploadEnabled",
        "datareporting.usage.uploadEnabled",
        "browser.crashReports.unsubmittedCheck.autoSubmit2",
      ];

      const prefsFilePath = PathUtils.join(recoveryPath, "prefs.js");
      const prefsBuffer = await IOUtils.read(prefsFilePath);
      const backupPrefs = PreferencesBackupResource.getPrefsFromBuffer(
        prefsBuffer,
        dataCollectionPrefs
      );
      const defaults = Services.prefs.getDefaultBranch(null);

      for (let pref of dataCollectionPrefs) {
        let groupPrefValue =
          await lazy.SelectableProfileService.getDBPref(pref);
        let backupPrefValue = backupPrefs.has(pref)
          ? backupPrefs.get(pref)
          : defaults.getBoolPref(pref, false);

        // the group has it enabled, but our backup has it disabled!
        if (groupPrefValue && !backupPrefValue) {
          Services.prefs.setBoolPref(pref, false);
        }
      }

      // Since the user might have messed with their prefs, let's make sure to
      // update the selectable profile specific ones (including the shared prefs db)
      await lazy.SelectableProfileService.addSelectableProfilePrefs(
        destProfilePath
      );
    }

    return null;
  }

  async measure(profilePath = PathUtils.profileDir) {
    const files = [
      "prefs.js",
      "xulstore.json",
      "containers.json",
      "customKeys.json",
      "handlers.json",
      "search.json.mozlz4",
      "user.js",
    ];
    let fullSize = 0;

    for (let filePath of files) {
      let resourcePath = PathUtils.join(profilePath, filePath);
      let resourceSize = await BackupResource.getFileSize(resourcePath);
      if (Number.isInteger(resourceSize)) {
        fullSize += resourceSize;
      }
    }

    const chromeDirectoryPath = PathUtils.join(profilePath, "chrome");
    let chromeDirectorySize =
      await BackupResource.getDirectorySize(chromeDirectoryPath);
    if (Number.isInteger(chromeDirectorySize)) {
      fullSize += chromeDirectorySize;
    }

    Glean.browserBackup.preferencesSize.set(fullSize);
  }
}
