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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
  BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs",
  LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs",
  MigrationWizardConstants:
    "chrome://browser/content/migration/migration-wizard-constants.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "gFluentStrings", function () {
  return new Localization([
    "branding/brand.ftl",
    "browser/migrationWizard.ftl",
  ]);
});

/**
 * Base class for a migration that involves reading a single file off of
 * the disk that the user picks using a file picker. The file might be
 * generated by another browser or some other application.
 */
export class FileMigratorBase {
  /**
   * This must be overridden to return a simple string identifier for the
   * migrator, for example "password-csv". This key is what
   * is used as an identifier when calling MigrationUtils.getFileMigrator.
   *
   * @type {string}
   */
  static get key() {
    throw new Error("FileMigrator.key must be overridden.");
  }

  /**
   * This must be overridden to return a Fluent string ID mapping to the display
   * name for this migrator. These strings should be defined in migrationWizard.ftl.
   *
   * @type {string}
   */
  static get displayNameL10nID() {
    throw new Error("FileMigrator.displayNameL10nID must be overridden.");
  }

  /**
   * This getter should get overridden to return an icon url to represent the
   * file to be imported from. By default, this will just use the default Favicon
   * image.
   *
   * @type {string}
   */
  static get brandImage() {
    return "chrome://global/skin/icons/defaultFavicon.svg";
  }

  /**
   * Returns true if the migrator is configured to be enabled.
   *
   * @type {boolean}
   *   true if the migrator should be shown in the migration wizard.
   */
  get enabled() {
    throw new Error("FileMigrator.enabled must be overridden.");
  }

  /**
   * This getter should be overridden to return a Fluent string ID for what
   * the migration wizard header should be while the file migration is
   * underway.
   *
   * @type {string}
   */
  get progressHeaderL10nID() {
    throw new Error("FileMigrator.progressHeaderL10nID must be overridden.");
  }

  /**
   * This getter should be overridden to return a Fluent string ID for what
   * the migration wizard header should be while the file migration is
   * done.
   *
   * @type {string}
   */
  get successHeaderL10nID() {
    throw new Error("FileMigrator.progressHeaderL10nID must be overridden.");
  }

  /**
   * @typedef {object} FilePickerConfiguration
   * @property {string} title
   *   The title that should be assigned to the native file picker window.
   * @property {FilePickerConfigurationFilter[]} filters
   *   One or more extension filters that should be applied to the native
   *   file picker window to make selection easier.
   */

  /**
   * @typedef {object} FilePickerConfigurationFilter
   * @property {string} title
   *   The title for the filter. Example: "CSV Files"
   * @property {string} extensionPattern
   *   A matching pattern for the filter. Example: "*.csv"
   */

  /**
   * A subclass of FileMigratorBase will eventually open a native file picker
   * for the user to select the file from their file system.
   *
   * Subclasses need to override this method in order to configure the
   * native file picker.
   *
   * @returns {Promise<FilePickerConfiguration>}
   */
  async getFilePickerConfig() {
    throw new Error("FileMigrator.getFilePickerConfig must be overridden.");
  }

  /**
   * Returns a list of one or more resource types that should appear to be
   * in progress of migrating while the file migration occurs. Notably,
   * this does not need to match the resource types that are returned by
   * `FileMigratorBase.migrate`.
   *
   * @type {string[]}
   *   An array of resource types from the
   *   MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES set.
   */
  get displayedResourceTypes() {
    throw new Error("FileMigrator.displayedResourceTypes must be overridden");
  }

  /**
   * Called to perform the file migration once the user makes a selection
   * from the native file picker. This will not be called if the user
   * chooses to cancel the native file picker.
   *
   * @param {string} _filePath
   *   The path that the user selected from the native file picker.
   */
  async migrate(_filePath) {
    throw new Error("FileMigrator.migrate must be overridden.");
  }
}

/**
 * A file migrator for importing passwords from CSV or TSV files. CSV
 * files are more common, so this is what we show as the file type for
 * the display name, but this FileMigrator accepts both.
 */
export class PasswordFileMigrator extends FileMigratorBase {
  static get key() {
    return "file-password-csv";
  }

  static get displayNameL10nID() {
    return "migration-wizard-migrator-display-name-file-password-csv";
  }

  static get brandImage() {
    return "chrome://branding/content/document.ico";
  }

  get enabled() {
    return true;
  }

  get displayedResourceTypes() {
    return [
      lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
        .PASSWORDS_FROM_FILE,
    ];
  }

  get progressHeaderL10nID() {
    return "migration-passwords-from-file-progress-header";
  }

  get successHeaderL10nID() {
    return "migration-passwords-from-file-success-header";
  }

  async getFilePickerConfig() {
    let [title, csvFilterTitle, tsvFilterTitle] =
      await lazy.gFluentStrings.formatValues([
        { id: "migration-passwords-from-file-picker-title" },
        { id: "migration-passwords-from-file-csv-filter-title" },
        { id: "migration-passwords-from-file-tsv-filter-title" },
      ]);

    return {
      title,
      filters: [
        {
          title: csvFilterTitle,
          extensionPattern: "*.csv",
        },
        {
          title: tsvFilterTitle,
          extensionPattern: "*.tsv",
        },
      ],
    };
  }

  async migrate(filePath) {
    try {
      let summary = await lazy.LoginCSVImport.importFromCSV(filePath);
      let newEntries = 0;
      let updatedEntries = 0;
      for (let entry of summary) {
        if (entry.result == "added") {
          newEntries++;
        } else if (entry.result == "modified") {
          updatedEntries++;
        }
      }
      let [newMessage, updatedMessage] = await lazy.gFluentStrings.formatValues(
        [
          {
            id: "migration-wizard-progress-success-new-passwords",
            args: { newEntries },
          },
          {
            id: "migration-wizard-progress-success-updated-passwords",
            args: { updatedEntries },
          },
        ]
      );

      Services.prefs.setBoolPref(
        "browser.migrate.interactions.csvpasswords",
        true
      );

      return {
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .PASSWORDS_NEW]: newMessage,
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .PASSWORDS_UPDATED]: updatedMessage,
      };
    } catch (e) {
      console.error(e);

      let errorMessage = await lazy.gFluentStrings.formatValue(
        "migration-passwords-from-file-no-valid-data"
      );
      throw new Error(errorMessage);
    }
  }
}

/**
 * A file migrator for importing bookmarks from a HTML or JSON file.
 *
 * @class BookmarksFileMigrator
 * @augments {FileMigratorBase}
 */
export class BookmarksFileMigrator extends FileMigratorBase {
  static get key() {
    return "file-bookmarks";
  }

  static get displayNameL10nID() {
    return "migration-wizard-migrator-display-name-file-bookmarks";
  }

  static get brandImage() {
    return "chrome://branding/content/document.ico";
  }

  get enabled() {
    return Services.prefs.getBoolPref(
      "browser.migrate.bookmarks-file.enabled",
      false
    );
  }

  get displayedResourceTypes() {
    return [
      lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
        .BOOKMARKS_FROM_FILE,
    ];
  }

  get progressHeaderL10nID() {
    return "migration-bookmarks-from-file-progress-header";
  }

  get successHeaderL10nID() {
    return "migration-bookmarks-from-file-success-header";
  }

  async getFilePickerConfig() {
    let [title, htmlFilterTitle, jsonFilterTitle] =
      await lazy.gFluentStrings.formatValues([
        { id: "migration-bookmarks-from-file-picker-title" },
        { id: "migration-bookmarks-from-file-html-filter-title" },
        { id: "migration-bookmarks-from-file-json-filter-title" },
      ]);

    return {
      title,
      filters: [
        {
          title: htmlFilterTitle,
          extensionPattern: "*.html",
        },
        {
          title: jsonFilterTitle,
          extensionPattern: "*.json",
        },
      ],
    };
  }

  async migrate(filePath) {
    try {
      let pathCheck = filePath.toLowerCase();
      let importedCount;

      if (pathCheck.endsWith("html")) {
        importedCount = await lazy.BookmarkHTMLUtils.importFromFile(filePath);
      } else if (pathCheck.endsWith("json") || pathCheck.endsWith("jsonlz4")) {
        importedCount = await lazy.BookmarkJSONUtils.importFromFile(filePath);
      }

      if (!importedCount) {
        // The catch will cause us to show a default error message.
        throw new Error();
      }

      let importedMessage = await lazy.gFluentStrings.formatValue(
        "migration-wizard-progress-success-new-bookmarks",
        {
          newEntries: importedCount,
        }
      );
      return {
        [lazy.MigrationWizardConstants.DISPLAYED_FILE_RESOURCE_TYPES
          .BOOKMARKS_FROM_FILE]: importedMessage,
      };
    } catch (e) {
      console.error(e);

      let errorMessage = await lazy.gFluentStrings.formatValue(
        "migration-bookmarks-from-file-no-valid-data"
      );
      throw new Error(errorMessage);
    }
  }
}
