/* 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";

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
  BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
  BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
  DistributionManagement: "resource:///modules/distribution.sys.mjs",
  PlacesBackups: "resource://gre/modules/PlacesBackups.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetters(lazy, {
  BrowserHandler: ["@mozilla.org/browser/clh;1", Ci.nsIBrowserHandler],
  UserIdleService: [
    "@mozilla.org/widget/useridleservice;1",
    Ci.nsIUserIdleService,
  ],
});

// Seconds of idle before trying to create a bookmarks backup.
const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
// Minimum interval between backups.  We try to not create more than one backup
// per interval.
const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;

export let PlacesBrowserStartup = {
  _migrationImportsDefaultBookmarks: false,
  _placesInitialized: false,
  _placesBrowserInitComplete: false,
  _isObservingIdle: false,
  _bookmarksBackupIdleTime: null,
  _firstWindowReady: Promise.withResolvers(),

  onFirstWindowReady(window) {
    this._firstWindowReady.resolve();
    // Set the default favicon size for UI views that use the page-icon protocol.
    lazy.PlacesUtils.favicons.setDefaultIconURIPreferredSize(
      16 * window.devicePixelRatio
    );
  },

  backendInitComplete() {
    if (!this._migrationImportsDefaultBookmarks) {
      this.initPlaces();
    }
  },

  willImportDefaultBookmarks() {
    this._migrationImportsDefaultBookmarks = true;
  },

  didImportDefaultBookmarks() {
    this.initPlaces({ initialMigrationPerformed: true });
  },

  /**
   * Initialize Places
   * - imports the bookmarks html file if bookmarks database is empty, try to
   *   restore bookmarks from a JSON backup if the backend indicates that the
   *   database was corrupt.
   *
   * These prefs can be set up by the frontend:
   *
   * WARNING: setting these preferences to true will overwite existing bookmarks
   *
   * - browser.places.importBookmarksHTML
   *   Set to true will import the bookmarks.html file from the profile folder.
   * - browser.bookmarks.restore_default_bookmarks
   *   Set to true by safe-mode dialog to indicate we must restore default
   *   bookmarks.
   *
   * @param {object} [options={}]
   * @param {boolean} [options.initialMigrationPerformed=false]
   *   Whether we performed an initial migration from another browser or via
   *   Firefox Refresh.
   */
  initPlaces({ initialMigrationPerformed = false } = {}) {
    if (this._placesInitialized) {
      throw new Error("Cannot initialize Places more than once");
    }
    this._placesInitialized = true;

    // We must instantiate the history service since it will tell us if we
    // need to import or restore bookmarks due to first-run, corruption or
    // forced migration (due to a major schema change).
    // If the database is corrupt or has been newly created we should
    // import bookmarks.
    let dbStatus = lazy.PlacesUtils.history.databaseStatus;

    // Show a notification with a "more info" link for a locked places.sqlite.
    if (dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_LOCKED) {
      // Note: initPlaces should always happen when the first window is ready,
      // in any case, better safe than sorry.
      this._firstWindowReady.promise.then(() => {
        this._showPlacesLockedNotificationBox();
        this._placesBrowserInitComplete = true;
        Services.obs.notifyObservers(null, "places-browser-init-complete");
      });
      return;
    }

    let importBookmarks =
      !initialMigrationPerformed &&
      (dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CREATE ||
        dbStatus == lazy.PlacesUtils.history.DATABASE_STATUS_CORRUPT);

    // Check if user or an extension has required to import bookmarks.html
    let importBookmarksHTML = false;
    try {
      importBookmarksHTML = Services.prefs.getBoolPref(
        "browser.places.importBookmarksHTML"
      );
      if (importBookmarksHTML) {
        importBookmarks = true;
      }
    } catch (ex) {}

    // Support legacy bookmarks.html format for apps that depend on that format.
    let autoExportHTML = Services.prefs.getBoolPref(
      "browser.bookmarks.autoExportHTML",
      false
    ); // Do not export.
    if (autoExportHTML) {
      // Sqlite.sys.mjs and Places shutdown happen at profile-before-change, thus,
      // to be on the safe side, this should run earlier.
      lazy.AsyncShutdown.profileChangeTeardown.addBlocker(
        "Places: export bookmarks.html",
        () =>
          lazy.BookmarkHTMLUtils.exportToFile(
            lazy.BookmarkHTMLUtils.defaultPath
          )
      );
    }

    (async () => {
      // Check if Safe Mode or the user has required to restore bookmarks from
      // default profile's bookmarks.html
      let restoreDefaultBookmarks = false;
      try {
        restoreDefaultBookmarks = Services.prefs.getBoolPref(
          "browser.bookmarks.restore_default_bookmarks"
        );
        if (restoreDefaultBookmarks) {
          // Ensure that we already have a bookmarks backup for today.
          await this._backupBookmarks();
          importBookmarks = true;
        }
      } catch (ex) {}

      // If the user did not require to restore default bookmarks, or import
      // from bookmarks.html, we will try to restore from JSON
      if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
        // get latest JSON backup
        let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup();
        if (lastBackupFile) {
          // restore from JSON backup
          await lazy.BookmarkJSONUtils.importFromFile(lastBackupFile, {
            replace: true,
            source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
          });
          importBookmarks = false;
        } else {
          // We have created a new database but we don't have any backup available
          importBookmarks = true;
          if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) {
            // If bookmarks.html is available in current profile import it...
            importBookmarksHTML = true;
          } else {
            // ...otherwise we will restore defaults
            restoreDefaultBookmarks = true;
          }
        }
      }

      // Import default bookmarks when necessary.
      // Otherwise, if any kind of import runs, default bookmarks creation should be
      // delayed till the import operations has finished.  Not doing so would
      // cause them to be overwritten by the newly imported bookmarks.
      if (!importBookmarks) {
        // Now apply distribution customized bookmarks.
        // This should always run after Places initialization.
        try {
          await lazy.DistributionManagement.applyBookmarks();
        } catch (e) {
          console.error(e);
        }
      } else {
        // An import operation is about to run.
        let bookmarksUrl = null;
        if (restoreDefaultBookmarks) {
          // User wants to restore the default set of bookmarks shipped with the
          // browser, those that new profiles start with.
          bookmarksUrl = "chrome://browser/content/default-bookmarks.html";
        } else if (await IOUtils.exists(lazy.BookmarkHTMLUtils.defaultPath)) {
          bookmarksUrl = PathUtils.toFileURI(
            lazy.BookmarkHTMLUtils.defaultPath
          );
        }

        if (bookmarksUrl) {
          // Import from bookmarks.html file.
          try {
            if (
              Services.policies.isAllowed("defaultBookmarks") &&
              // Default bookmarks are imported after startup, and they may
              // influence the outcome of tests, thus it's possible to use
              // this test-only pref to skip the import.
              !(
                Cu.isInAutomation &&
                Services.prefs.getBoolPref(
                  "browser.bookmarks.testing.skipDefaultBookmarksImport",
                  false
                )
              )
            ) {
              await lazy.BookmarkHTMLUtils.importFromURL(bookmarksUrl, {
                replace: true,
                source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
              });
            }
          } catch (e) {
            console.error("Bookmarks.html file could be corrupt. ", e);
          }
          try {
            // Now apply distribution customized bookmarks.
            // This should always run after Places initialization.
            await lazy.DistributionManagement.applyBookmarks();
          } catch (e) {
            console.error(e);
          }
        } else {
          console.error(new Error("Unable to find bookmarks.html file."));
        }

        // Reset preferences, so we won't try to import again at next run
        if (importBookmarksHTML) {
          Services.prefs.setBoolPref(
            "browser.places.importBookmarksHTML",
            false
          );
        }
        if (restoreDefaultBookmarks) {
          Services.prefs.setBoolPref(
            "browser.bookmarks.restore_default_bookmarks",
            false
          );
        }
      }

      // Initialize bookmark archiving on idle.
      // If the last backup has been created before the last browser session,
      // and is days old, be more aggressive with the idle timer.
      let idleTime = BOOKMARKS_BACKUP_IDLE_TIME_SEC;
      if (!(await lazy.PlacesBackups.hasRecentBackup())) {
        idleTime /= 2;
      }

      if (!this._isObservingIdle) {
        lazy.UserIdleService.addIdleObserver(this._backupBookmarks, idleTime);
        Services.obs.addObserver(this, "profile-before-change");
        this._isObservingIdle = true;
      }

      this._bookmarksBackupIdleTime = idleTime;

      if (this._isNewProfile) {
        // New profiles may have existing bookmarks (imported from another browser or
        // copied into the profile) and we want to show the bookmark toolbar for them
        // in some cases.
        await lazy.PlacesUIUtils.maybeToggleBookmarkToolbarVisibility();
      }
    })()
      .catch(ex => {
        console.error(ex);
      })
      .then(() => {
        // NB: deliberately after the catch so that we always do this, even if
        // we threw halfway through initializing in the Task above.
        this._placesBrowserInitComplete = true;
        Services.obs.notifyObservers(null, "places-browser-init-complete");
      });
  },

  /**
   * If a backup for today doesn't exist, this creates one.
   */
  async _backupBookmarks() {
    let lastBackupFile = await lazy.PlacesBackups.getMostRecentBackup();
    // Should backup bookmarks if there are no backups or the maximum
    // interval between backups elapsed.
    if (
      !lastBackupFile ||
      Date.now() - lazy.PlacesBackups.getDateForFile(lastBackupFile).getTime() >
        BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS * 86400000
    ) {
      let maxBackups = Services.prefs.getIntPref(
        "browser.bookmarks.max_backups"
      );
      await lazy.PlacesBackups.create(maxBackups);
    }
  },

  /**
   * Show the notificationBox for a locked places database.
   */
  async _showPlacesLockedNotificationBox() {
    var win = lazy.BrowserWindowTracker.getTopWindow({
      allowFromInactiveWorkspace: true,
    });
    var buttons = [{ supportPage: "places-locked" }];

    var notifyBox = win.gBrowser.getNotificationBox();
    var notification = await notifyBox.appendNotification(
      "places-locked",
      {
        label: { "l10n-id": "places-locked-prompt" },
        priority: win.gNotificationBox.PRIORITY_CRITICAL_MEDIUM,
      },
      buttons
    );
    notification.persistence = -1; // Until user closes it
  },

  notifyIfInitializationComplete() {
    if (this._placesBrowserInitComplete) {
      Services.obs.notifyObservers(null, "places-browser-init-complete");
    }
  },

  async maybeAddImportButton() {
    // First check if we've already added the import button, in which
    // case we should check for events indicating we can remove it.
    if (
      Services.prefs.getBoolPref("browser.bookmarks.addedImportButton", false)
    ) {
      lazy.PlacesUIUtils.removeImportButtonWhenImportSucceeds();
      return;
    }

    // Otherwise, check if this is a new profile where we need to add it.
    // `maybeAddImportButton` will call
    // `removeImportButtonWhenImportSucceeds`itself if/when it adds the
    // button. Doing things in this order avoids listening for removal
    // more than once.
    if (
      lazy.BrowserHandler.firstRunProfile &&
      // Not in automation: the button changes CUI state, breaking tests
      !Cu.isInAutomation
    ) {
      await lazy.PlacesUIUtils.maybeAddImportButton();
    }
  },

  handleShutdown() {
    if (this._bookmarksBackupIdleTime) {
      lazy.UserIdleService.removeIdleObserver(
        this._backupBookmarks,
        this._bookmarksBackupIdleTime
      );
      this._bookmarksBackupIdleTime = null;
    }
  },

  observe(subject, topic, _data) {
    switch (topic) {
      case "profile-before-change":
        this.handleShutdown();
        break;
    }
  },
};

PlacesBrowserStartup._backupBookmarks =
  PlacesBrowserStartup._backupBookmarks.bind(PlacesBrowserStartup);

PlacesBrowserStartup.QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]);
