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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
  QuickSuggestRemoteSettings:
    "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
  SuggestionsMap:
    "resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
  UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
});

const RESULT_MENU_COMMAND = {
  HELP: "help",
  NOT_INTERESTED: "not_interested",
  NOT_RELEVANT: "not_relevant",
  SHOW_LESS_FREQUENTLY: "show_less_frequently",
};

/**
 * A feature that manages Pocket suggestions in remote settings.
 */
export class PocketSuggestions extends BaseFeature {
  constructor() {
    super();
    this.#lowConfidenceSuggestionsMap = new lazy.SuggestionsMap();
    this.#highConfidenceSuggestionsMap = new lazy.SuggestionsMap();
  }

  get shouldEnable() {
    return (
      lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled") &&
      lazy.UrlbarPrefs.get("pocketFeatureGate") &&
      lazy.UrlbarPrefs.get("suggest.pocket") &&
      lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored")
    );
  }

  get enablingPreferences() {
    return ["suggest.pocket", "suggest.quicksuggest.nonsponsored"];
  }

  get merinoProvider() {
    return "pocket";
  }

  get showLessFrequentlyCount() {
    let count = lazy.UrlbarPrefs.get("pocket.showLessFrequentlyCount") || 0;
    return Math.max(count, 0);
  }

  get canShowLessFrequently() {
    let cap =
      lazy.UrlbarPrefs.get("pocketShowLessFrequentlyCap") ||
      lazy.QuickSuggestRemoteSettings.config.show_less_frequently_cap ||
      0;
    return !cap || this.showLessFrequentlyCount < cap;
  }

  enable(enabled) {
    if (enabled) {
      lazy.QuickSuggestRemoteSettings.register(this);
    } else {
      lazy.QuickSuggestRemoteSettings.unregister(this);
      this.#lowConfidenceSuggestionsMap.clear();
      this.#highConfidenceSuggestionsMap.clear();
    }
  }

  async queryRemoteSettings(searchString) {
    // If the search string matches high confidence suggestions, they should be
    // treated as top picks. Otherwise try to match low confidence suggestions.
    let is_top_pick = false;
    let suggestions = this.#highConfidenceSuggestionsMap.get(searchString);
    if (suggestions.length) {
      is_top_pick = true;
    } else {
      suggestions = this.#lowConfidenceSuggestionsMap.get(searchString);
    }

    let lowerSearchString = searchString.toLocaleLowerCase();
    return suggestions.map(suggestion => {
      // Add `full_keyword` to each matched suggestion. It should be the longest
      // keyword that starts with the user's search string.
      let full_keyword = lowerSearchString;
      let keywords = is_top_pick
        ? suggestion.highConfidenceKeywords
        : suggestion.lowConfidenceKeywords;
      for (let keyword of keywords) {
        if (
          keyword.startsWith(lowerSearchString) &&
          full_keyword.length < keyword.length
        ) {
          full_keyword = keyword;
        }
      }
      return { ...suggestion, is_top_pick, full_keyword };
    });
  }

  async onRemoteSettingsSync(rs) {
    let records = await rs.get({ filters: { type: "pocket-suggestions" } });
    if (!this.isEnabled) {
      return;
    }

    let lowMap = new lazy.SuggestionsMap();
    let highMap = new lazy.SuggestionsMap();

    this.logger.debug(`Got ${records.length} records`);
    for (let record of records) {
      let { buffer } = await rs.attachments.download(record);
      if (!this.isEnabled) {
        return;
      }

      let suggestions = JSON.parse(new TextDecoder("utf-8").decode(buffer));
      this.logger.debug(`Adding ${suggestions.length} suggestions`);

      await lowMap.add(suggestions, {
        keywordsProperty: "lowConfidenceKeywords",
        mapKeyword:
          lazy.SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD,
      });
      if (!this.isEnabled) {
        return;
      }

      await highMap.add(suggestions, {
        keywordsProperty: "highConfidenceKeywords",
      });
      if (!this.isEnabled) {
        return;
      }
    }

    this.#lowConfidenceSuggestionsMap = lowMap;
    this.#highConfidenceSuggestionsMap = highMap;
  }

  makeResult(queryContext, suggestion, searchString) {
    if (!this.isEnabled) {
      // The feature is disabled on the client, but Merino may still return
      // suggestions anyway, and we filter them out here.
      return null;
    }

    // If the user hasn't clicked the "Show less frequently" command, the
    // suggestion can be shown. Otherwise, the suggestion can be shown if the
    // user typed more than one word with at least `showLessFrequentlyCount`
    // characters after the first word, including spaces.
    if (this.showLessFrequentlyCount) {
      let spaceIndex = searchString.search(/\s/);
      if (
        spaceIndex < 0 ||
        searchString.length - spaceIndex < this.showLessFrequentlyCount
      ) {
        return null;
      }
    }

    let isBestMatch =
      suggestion.is_top_pick &&
      lazy.UrlbarPrefs.get("bestMatchEnabled") &&
      lazy.UrlbarPrefs.get("suggest.bestmatch");

    return Object.assign(
      new lazy.UrlbarResult(
        lazy.UrlbarUtils.RESULT_TYPE.URL,
        lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
        ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
          url: suggestion.url,
          title: [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED],
          description: isBestMatch ? suggestion.description : "",
          icon: "chrome://global/skin/icons/pocket.svg",
          shouldShowUrl: true,
          bottomTextL10n: {
            id: "firefox-suggest-pocket-bottom-text",
            args: {
              keywordSubstringTyped: searchString,
              keywordSubstringNotTyped: suggestion.full_keyword.substring(
                searchString.length
              ),
            },
          },
          helpUrl: lazy.QuickSuggest.HELP_URL,
        })
      ),
      {
        isRichSuggestion: true,
        richSuggestionIconSize: isBestMatch ? 24 : 16,
        showFeedbackMenu: true,
      }
    );
  }

  handleCommand(queryContext, result, selType) {
    switch (selType) {
      case RESULT_MENU_COMMAND.HELP:
        // "help" is handled by UrlbarInput, no need to do anything here.
        break;
      // selType == "dismiss" when the user presses the dismiss key shortcut.
      case "dismiss":
      case RESULT_MENU_COMMAND.NOT_RELEVANT:
        lazy.QuickSuggest.blockedSuggestions.add(result.payload.url);
        queryContext.view.acknowledgeDismissal(result, false);
        break;
      case RESULT_MENU_COMMAND.NOT_INTERESTED:
        lazy.UrlbarPrefs.set("suggest.pocket", false);
        queryContext.view.acknowledgeDismissal(result, true);
        break;
      case RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY:
        queryContext.view.acknowledgeFeedback(result);
        this.incrementShowLessFrequentlyCount();
        break;
    }
  }

  getResultCommands(result) {
    let commands = [];

    if (!result.isBestMatch && this.canShowLessFrequently) {
      commands.push({
        name: RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY,
        l10n: {
          id: "firefox-suggest-command-show-less-frequently",
        },
      });
    }

    commands.push(
      {
        l10n: {
          id: "firefox-suggest-command-dont-show-this",
        },
        children: [
          {
            name: RESULT_MENU_COMMAND.NOT_RELEVANT,
            l10n: {
              id: "firefox-suggest-command-not-relevant",
            },
          },
          {
            name: RESULT_MENU_COMMAND.NOT_INTERESTED,
            l10n: {
              id: "firefox-suggest-command-not-interested",
            },
          },
        ],
      },
      { name: "separator" },
      {
        name: RESULT_MENU_COMMAND.HELP,
        l10n: {
          id: "urlbar-result-menu-learn-more-about-firefox-suggest",
        },
      }
    );

    return commands;
  }

  incrementShowLessFrequentlyCount() {
    if (this.canShowLessFrequently) {
      lazy.UrlbarPrefs.set(
        "pocket.showLessFrequentlyCount",
        this.showLessFrequentlyCount + 1
      );
    }
  }

  #lowConfidenceSuggestionsMap;
  #highConfidenceSuggestionsMap;
}
