/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

import {
  BackupResource,
  bytesToFuzzyKilobytes,
} from "resource:///modules/backup/BackupResource.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
  TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
});

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

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "TAB_FLUSH_TIMEOUT",
  "browser.backup.tab-flush-timeout",
  5000
);

/**
 * Class representing Session store related files within a user profile.
 */
export class SessionStoreBackupResource extends BackupResource {
  // Allow creator to provide a "SessionStore" object, so we can use mocks in
  // testing.  Passing `null` means use the real service.
  constructor(sessionStore = null) {
    super();
    this._sessionStore = sessionStore;
  }

  static get key() {
    return "sessionstore";
  }

  static get requiresEncryption() {
    // Session store data does not require encryption, but if encryption is
    // disabled, then session cookies will be cleared from the backup before
    // writing it to the disk.
    return false;
  }

  get #sessionStore() {
    return this._sessionStore || lazy.SessionStore;
  }

  get filteredSessionStoreState() {
    let sessionStoreState = this.#sessionStore.getCurrentState(true);
    // Preserving session cookies in a backup used on a different machine
    // may break behavior for websites. So we leave them out of the backup.
    sessionStoreState.cookies = [];

    // Remove session storage.
    if (sessionStoreState.windows) {
      // We don't want to backup private windows
      sessionStoreState.windows = sessionStoreState.windows.filter(
        w => !w?.isPrivate
      );
      sessionStoreState.windows.forEach(win => {
        if (win.tabs) {
          win.tabs.forEach(tab => delete tab.storage);
        }
        if (win._closedTabs) {
          win._closedTabs.forEach(closedTab => delete closedTab.state.storage);
        }
      });
    }
    if (sessionStoreState.savedGroups) {
      sessionStoreState.savedGroups.forEach(group => {
        if (group.tabs) {
          group.tabs.forEach(tab => delete tab.state.storage);
        }
      });
    }

    return sessionStoreState;
  }

  async backup(
    stagingPath,
    profilePath = PathUtils.profileDir,
    _isEncrypting = false
  ) {
    // Flush tab state so backups receive the correct url to restore.
    await Promise.race([
      Promise.allSettled(
        lazy.BrowserWindowTracker.orderedWindows.map(
          lazy.TabStateFlusher.flushWindow
        )
      ),
      new Promise((_, reject) =>
        lazy.setTimeout(reject, lazy.TAB_FLUSH_TIMEOUT, { timeout: true })
      ),
    ]).catch(e => {
      if (e?.timeout) {
        lazy.logConsole.warn("Timed out waiting while flushing tab state.");
      } else {
        lazy.logConsole.error(
          "Unrecognized error while flushing tab state.",
          e
        );
      }
    });

    let sessionStorePath = PathUtils.join(stagingPath, "sessionstore.jsonlz4");

    await IOUtils.writeJSON(sessionStorePath, this.filteredSessionStoreState, {
      compress: true,
    });
    await BackupResource.copyFiles(profilePath, stagingPath, [
      "sessionstore-backups",
    ]);

    return null;
  }

  async recover(_manifestEntry, recoveryPath, destProfilePath) {
    await BackupResource.copyFiles(recoveryPath, destProfilePath, [
      "sessionstore.jsonlz4",
      "sessionstore-backups",
    ]);

    return null;
  }

  async measure(profilePath = PathUtils.profileDir) {
    // Get the current state of the session store JSON and
    // measure it's uncompressed size.
    let sessionStoreJson = this.#sessionStore.getCurrentState(true);
    let sessionStoreSize = new TextEncoder().encode(
      JSON.stringify(sessionStoreJson)
    ).byteLength;
    let sessionStoreNearestTenKb = bytesToFuzzyKilobytes(sessionStoreSize);

    Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenKb);

    let sessionStoreBackupsDirectoryPath = PathUtils.join(
      profilePath,
      "sessionstore-backups"
    );
    let sessionStoreBackupsDirectorySize =
      await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath);

    Glean.browserBackup.sessionStoreBackupsDirectorySize.set(
      sessionStoreBackupsDirectorySize
    );
  }
}
