/* 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 { SuggestProvider } from "moz-src:///browser/components/urlbar/private/SuggestFeature.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs",
  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
  UrlbarSearchUtils:
    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
});

const SHOW_COUNTDOWN_THRESHOLD_DAYS = 30;
const MS_PER_DAY = 1000 * 60 * 60 * 24;

/**
 * A Suggest feature that manages important-dates suggestions.
 */
export class ImportantDatesSuggestions extends SuggestProvider {
  get enablingPreferences() {
    return ["importantDatesFeatureGate", "suggest.importantDates"];
  }

  get primaryUserControlledPreferences() {
    return ["suggest.importantDates"];
  }

  get rustSuggestionType() {
    return "Dynamic";
  }

  get dynamicRustSuggestionTypes() {
    return ["important_dates"];
  }

  isSuggestionSponsored(suggestion) {
    if (suggestion.data?.result?.payload?.hasOwnProperty("isSponsored")) {
      return suggestion.data.result.payload.isSponsored;
    }
    return false;
  }

  getSuggestionTelemetryType(suggestion) {
    if (suggestion.data?.result?.payload?.hasOwnProperty("telemetryType")) {
      return suggestion.data.result.payload.telemetryType;
    }
    return this.dynamicRustSuggestionTypes[0];
  }

  async makeResult(queryContext, suggestion, _searchString) {
    if (
      !suggestion.data?.result?.payload ||
      typeof suggestion.data.result.payload != "object"
    ) {
      this.logger.warn("Unexpected remote settings suggestion");
      return null;
    }

    return this.#makeDateResult(queryContext, suggestion.data.result.payload);
  }

  /**
   * @typedef DatePayload
   *   The shape of an important dates suggestion payload.
   * @property {(string | [string, string])[]} dates
   *   An array of dates or date tuples in the format "YYYY-MM-DD".
   *   For a date tuple, the first value represents the start and the second
   *   the end date. This is assumed to be ordered oldest to newest.
   * @property {string} name
   *   The name of the event.
   */

  /**
   * Returns a string about the date of an event that can be displayed.
   *
   * @param {string | [string, string]} dateStr
   *   A string in the format "YYYY-MM-DD" representing a single
   *   day or a tuple of strings representing start and end days.
   * @returns {string}
   *   A localized string about the date of the event.
   */
  #formatDateOrRange(dateStr) {
    if (Array.isArray(dateStr)) {
      let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, {
        year: "numeric",
        month: "long",
        day: "numeric",
      });
      let startDate = new Date(dateStr[0] + "T00:00");
      let endDate = new Date(dateStr[1] + "T00:00");
      return format.formatRange(startDate, endDate);
    }

    let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, {
      weekday: "long",
      year: "numeric",
      month: "long",
      day: "numeric",
    });
    let date = new Date(dateStr + "T00:00");
    return format.format(date);
  }

  /**
   * Returns a l10n object about the name of and time until an event
   * that can be used as the description of a urlbar result.
   *
   * @param {string | [string, string]} dateStr
   *   A string in the format "YYYY-MM-DD" representing a single
   *   day or a tuple of strings representing start and end days.
   * @param {string} name
   *   The name of the event.
   * @returns {?object}
   *   A l10n object or null if the event is in the past.
   */
  #formatDateCountdown(dateStr, name) {
    if (Array.isArray(dateStr)) {
      let daysUntilStart = this.#getDaysUntil(dateStr[0]);
      let daysUntilEnd = this.#getDaysUntil(dateStr[1]);
      if (daysUntilEnd < 0) {
        throw new Error("Date lies in the past.");
      }
      if (daysUntilStart > 0) {
        return {
          id: "urlbar-result-dates-countdown-range",
          args: { daysUntilStart, name },
        };
      }
      if (daysUntilEnd > 0) {
        return {
          id: "urlbar-result-dates-ongoing",
          args: { daysUntilEnd, name },
        };
      }
      return {
        id: "urlbar-result-dates-ends-today",
        args: { name },
      };
    }

    let daysUntil = this.#getDaysUntil(dateStr);
    if (daysUntil < 0) {
      throw new Error("Date lies in the past.");
    }
    if (daysUntil > 0) {
      return {
        id: "urlbar-result-dates-countdown",
        args: { daysUntilStart: daysUntil, name },
      };
    }
    return {
      id: "urlbar-result-dates-today",
      args: { name },
    };
  }

  /**
   * Returns the number of days until the specified date.
   *
   * @param {string} dateStr
   *   A string in the format "YYYY-MM-DD".
   * @returns {number}
   *   The time until the input date in days.
   */
  #getDaysUntil(dateStr) {
    let now = new Date();
    now.setHours(0, 0, 0, 0);
    let date = new Date(dateStr + "T00:00");

    let msUntil = date.getTime() - now.getTime();
    // Round to account for potential DST.
    return Math.round(msUntil / MS_PER_DAY);
  }

  /**
   * Creates a urlbar result from an important dates payload.
   *
   * @param {UrlbarQueryContext} queryContext
   *   The query context.
   * @param {DatePayload} payload
   *   The important dates payload.
   * @returns {?UrlbarResult}
   *   A urlbar result or null if all instances are in the past.
   */
  #makeDateResult(queryContext, payload) {
    let eventDateOrRange = payload.dates.find(
      // Find first entry where the end date is in the future.
      // This is always the upcoming occurrence since dates is ordered by time.
      d => this.#getDaysUntil(Array.isArray(d) ? d[1] : d) >= 0
    );
    if (!eventDateOrRange) {
      // All instances of the event are in the past.
      return null;
    }

    let daysUntilStart = this.#getDaysUntil(
      Array.isArray(eventDateOrRange) ? eventDateOrRange[0] : eventDateOrRange
    );

    let description, descriptionL10n;
    if (daysUntilStart > SHOW_COUNTDOWN_THRESHOLD_DAYS) {
      description = payload.name;
    } else {
      descriptionL10n = {
        ...this.#formatDateCountdown(eventDateOrRange, payload.name),
      };
    }

    let dateString = this.#formatDateOrRange(eventDateOrRange);
    return new lazy.UrlbarResult({
      type: lazy.UrlbarUtils.RESULT_TYPE.SEARCH,
      source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
      isBestMatch: true,
      hideRowLabel: true,
      richSuggestionIconSize: 24,
      payload: {
        title: dateString,
        description,
        engine: lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate)
          .name,
        descriptionL10n,
        query: payload.name,
        lowerCaseSuggestion: payload.name.toLowerCase(),
        icon: "chrome://browser/skin/calendar-24.svg",
        helpUrl: lazy.QuickSuggest.HELP_URL,
        isManageable: true,
        isBlockable: true,
      },
      payloadHighlights: {
        title: [
          // Make whole title bold.
          [0, dateString.length],
        ],
      },
    });
  }

  onEngagement(_queryContext, controller, details, _searchString) {
    switch (details.selType) {
      case "manage":
        // "manage" is handled by UrlbarInput, no need to do anything here.
        break;
      case "dismiss": {
        let { result } = details;
        lazy.QuickSuggest.dismissResult(result);
        result.acknowledgeDismissalL10n = {
          id: "firefox-suggest-dismissal-acknowledgment-one",
        };
        controller.removeResult(result);
        break;
      }
    }
  }
}
