/* 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 = {};
const PREF_APP_UPDATE_COMPULSORY_RESTART = "app.update.compulsory_restart";
let deferredRestartTasks = null;

ChromeUtils.defineESModuleGetters(lazy, {
  ScheduledTask: "resource://gre/modules/ScheduledTask.sys.mjs",
  InfoBar: "resource:///modules/asrouter/InfoBar.sys.mjs",
});

// Forcibly close Firefox, without waiting for beforeunload handlers.
function forceRestart() {
  Services.startup.quit(
    Services.startup.eForceQuit | Services.startup.eRestart
  );
  console.error(`Firefox is restarting`);
}

function infobarDispatchCallback(action, _selectedBrowser) {
  if (action?.type === "USER_ACTION" && action.data?.type === "RESTART_APP") {
    forceRestart();
  }
}

// Display the notification message
function showNotificationToolbar(restartZonedDateTime) {
  const datetime = restartZonedDateTime.epochMilliseconds;

  const message = {
    weight: 100,
    id: "COMPULSORY_RESTART_SCHEDULED",
    content: {
      priority: 3,
      type: "universal",
      dismissable: false,
      text: {
        string_id: "compulsory-restart-message",
      },
      buttons: [
        {
          label: {
            string_id: "policy-update-now",
          },
          action: {
            type: "RESTART_APP",
            dismiss: false,
          },
        },
      ],
      attributes: {
        datetime,
      },
    },
    template: "infobar",
    targeting: "true",
    groups: [],
  };

  const win = Services.wm.getMostRecentBrowserWindow();
  if (!win) {
    return;
  }
  lazy.InfoBar.showInfoBarMessage(
    win.gBrowser.selectedBrowser,
    message,
    infobarDispatchCallback
  );
}

/**
 * Disarm deferred tasks and clear the state for the next test.
 */
export function testingOnly_resetTasks() {
  if (!Cu.isInAutomation) {
    throw new Error("this method only usable in testing");
  }
  deferredRestartTasks?.notificationTask?.disarm();
  deferredRestartTasks?.restartTask?.disarm();
  deferredRestartTasks = null;
}

/**
 * Check on whether the deferred tasks have been created and armed.
 */
export function testingOnly_getTaskStatus() {
  if (!Cu.isInAutomation) {
    throw new Error("this method only usable in testing");
  }
  if (deferredRestartTasks) {
    const res = {
      notificationTask: deferredRestartTasks.notificationTask?.isArmed,
      restartTask: deferredRestartTasks.restartTask?.isArmed,
    };
    return res;
  }
  return null;
}

// Example settings:
// {NotificationPeriodHours: 72, RestartTimeOfDay: {Hour: 18, Minute: 45}}

/**
 * Params:
 * nowInstant: A Temporal.Instant object representing the current time.
 * notificationPeriodHours: The minimum amount of time in hours between when an update has been staged and when a warning will be displayed
 * restartTimeOfDay: The time of day, in local time, that the restart will take place, in the form of an object like: {hour: 3, minute: 14}
 *
 * Returns:
 * {
 *  notificationZonedDateTime: a Temporal.ZonedDateTime object representing the time when the notification bar should start to be shown.
 *  restartZonedDateTime: a Temporal.ZonedDateTime object representing the time when the current Firefox instance will forcibly quit
 * }
 */
export function calculateSchedule(
  nowInstant,
  notificationPeriodHours,
  restartTimeOfDay
) {
  // The notification time is nowInstant + notificationPeriodHours
  const notificationDelay = Temporal.Duration.from({
    hours: notificationPeriodHours,
  });
  const notificationInstant = nowInstant.add(notificationDelay);
  const notificationZonedDateTime = notificationInstant.toZonedDateTimeISO(
    Temporal.Now.timeZoneId()
  );

  // Figure out the compulsory restart time. The earliest is 1 hour past the notification time, the latest is 25 hours past the notification time.
  // restart time = if (there is an upcoming `restartTimeOfDay` on the notification day, an it's more than an hour in the future) then (notification day at restart time) else (notification day + 1 at restart time)
  const restartTime = Temporal.PlainTime.from({
    hour: restartTimeOfDay.Hour,
    minute: restartTimeOfDay.Minute,
  });

  let restartZonedDateTime =
    notificationZonedDateTime.withPlainTime(restartTime);

  // Make sure the user has at least 1 hour notification before restart time.
  // If not, postpone the restart by 24 hours.
  if (
    Temporal.Duration.compare(
      notificationZonedDateTime.until(restartZonedDateTime),
      Temporal.Duration.from({ hours: 1 })
    ) < 0
  ) {
    // it's less than 1 hour until the restart time.
    restartZonedDateTime = restartZonedDateTime.add(
      Temporal.Duration.from({ hours: 24 })
    );
  }

  return { notificationZonedDateTime, restartZonedDateTime };
}

// Create scheduled tasks with requested date/time
export function createScheduledRestartTasks(
  restartZonedDateTime,
  notificationZonedDateTime
) {
  const notificationTask = new lazy.ScheduledTask(() => {
    showNotificationToolbar(restartZonedDateTime);
  }, notificationZonedDateTime.epochMilliseconds);
  const restartTask = new lazy.ScheduledTask(
    forceRestart,
    restartZonedDateTime.epochMilliseconds
  );
  notificationTask.arm();
  restartTask.arm();
  return { notificationTask, restartTask };
}

// Read the policy from prefs and parse the JSON.
export function getCompulsoryRestartPolicy() {
  const compulsoryRestartSettingStr = Services.prefs.getStringPref(
    PREF_APP_UPDATE_COMPULSORY_RESTART,
    null
  );
  if (compulsoryRestartSettingStr) {
    const compulsoryRestartSetting = JSON.parse(compulsoryRestartSettingStr);
    if (
      typeof compulsoryRestartSetting?.NotificationPeriodHours === "number" &&
      typeof compulsoryRestartSetting?.RestartTimeOfDay === "object" &&
      typeof compulsoryRestartSetting.RestartTimeOfDay.Hour === "number" &&
      typeof compulsoryRestartSetting.RestartTimeOfDay.Minute === "number"
    ) {
      return compulsoryRestartSetting;
    }
  }
  return null;
}

// This is the main entry point into this module.
// This function is called when an update is staged, to set timers for
// when to show the notification and when to force a restart.
export function handleCompulsoryUpdatePolicy() {
  if (!deferredRestartTasks) {
    const compulsoryRestartSetting = getCompulsoryRestartPolicy();
    if (compulsoryRestartSetting) {
      const now = Temporal.Now.instant();
      const { restartZonedDateTime, notificationZonedDateTime } =
        calculateSchedule(
          now,
          compulsoryRestartSetting.NotificationPeriodHours,
          compulsoryRestartSetting.RestartTimeOfDay
        );
      if (restartZonedDateTime && notificationZonedDateTime) {
        deferredRestartTasks = createScheduledRestartTasks(
          restartZonedDateTime,
          notificationZonedDateTime
        );
      } else {
        console.error(
          `Invalid restart settings: ${JSON.stringify(compulsoryRestartSetting)}`
        );
      }
    }
  }
}

const observer = {
  observe: (_subject, topic, _data) => {
    switch (topic) {
      case "update-downloaded":
      case "update-staged":
        handleCompulsoryUpdatePolicy();
        break;
    }
  },
};

export const UpdatePolicyEnforcer = {
  registerObservers() {
    Services.obs.addObserver(observer, "update-downloaded");
    Services.obs.addObserver(observer, "update-staged");
  },
};
