/* 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 https://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

/** @type {lazy} */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
  return console.createInstance({
    prefix: "CaptchaDetectionPingUtils",
    maxLogLevelPref: "captchadetection.loglevel",
  });
});

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

const HAS_UNSUBMITTED_DATA_PREF = "captchadetection.hasUnsubmittedData";
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "hasUnsubmittedData",
  HAS_UNSUBMITTED_DATA_PREF,
  false
);

const SUBMISSION_INTERVAL_PREF = "captchadetection.submissionInterval";
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "submissionInterval",
  SUBMISSION_INTERVAL_PREF,
  // See i32SafeDate() function for why we divide by 1000.
  Math.floor((24 * 60 * 60) / 1000)
);

const LAST_SUBMISSION_PREF = "captchadetection.lastSubmission";
XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "lastSubmission",
  LAST_SUBMISSION_PREF,
  0
);

/**
 * Utility class for handling the Captcha Detection ping.
 */
export class CaptchaDetectionPingUtils {
  static #setHasUnsubmittedDataFlag() {
    if (lazy.hasUnsubmittedData) {
      return;
    }

    Services.prefs.setBoolPref(HAS_UNSUBMITTED_DATA_PREF, true);
    CaptchaDetectionPingUtils.#setPrivacyMetrics();
  }

  static #setPrivacyMetrics() {
    lazy.console.debug("Setting privacy metrics.");
    for (const [metricName, { type, name }] of Object.entries(
      CaptchaDetectionPingUtils.prefsOfInterest
    )) {
      Glean.captchaDetection[metricName].set(
        Services.prefs["get" + type + "Pref"](name)
      );
    }
  }

  static i32SafeDate() {
    // Prefs int values are 32-bit signed integers, so we can't store the full
    // Date.now(). We could divide by 1000, but that is safe until 2038, after
    // which it will overflow. Dividing by 1000 again will make it safe until
    // a lot longer.
    // Dates will be off by (at most) about 1000 seconds, 16.6 minutes, but that's fine.
    return Math.floor(Date.now() / 1000 / 1000);
  }

  static flushPing(_subject, topic, prefName) {
    if (
      topic === "nsPref:changed" &&
      !Object.values(CaptchaDetectionPingUtils.prefsOfInterest).some(
        pref => pref.name === prefName
      )
    ) {
      // Flush ping is called from the observer service, and we don't want to
      // submit the ping if the pref that changed is not of interest.
      lazy.console.debug("Pref that changed is not of interest for the ping.");
      return;
    }

    if (!lazy.hasUnsubmittedData) {
      lazy.console.debug("No unsubmitted data to submit.");
      return;
    }

    if (!CaptchaDetectionPingUtils.profileIsOpen) {
      lazy.console.debug(
        "Not submitting ping because profile is closing or already closed."
      );
      return;
    }

    lazy.console.debug("Flushing ping.");
    GleanPings.captchaDetection.submit();

    lazy.console.debug("Setting unsubmitted data flag to false.");
    Services.prefs.setBoolPref(HAS_UNSUBMITTED_DATA_PREF, false);

    lazy.console.debug("Setting lastSubmission to now.");
    Services.prefs.setIntPref(
      LAST_SUBMISSION_PREF,
      CaptchaDetectionPingUtils.i32SafeDate()
    );
  }

  static maybeSubmitPing(setHasUnsubmittedDataFlag = true) {
    if (!CaptchaDetectionPingUtils.profileIsOpen) {
      lazy.console.debug(
        "Not submitting ping because profile is closing or already closed."
      );
      return;
    }

    if (setHasUnsubmittedDataFlag) {
      CaptchaDetectionPingUtils.#setHasUnsubmittedDataFlag();
    }

    const lastSubmission = lazy.lastSubmission;
    if (lastSubmission === 0) {
      // If this is the first time maybeSubmitPing is called, set the lastSubmission time to now
      // so that we don't submit a ping with just one event.
      lazy.console.debug("Setting lastSubmission to now.");
      Services.prefs.setIntPref(
        LAST_SUBMISSION_PREF,
        CaptchaDetectionPingUtils.i32SafeDate()
      );
      return;
    }

    if (
      lastSubmission >
      CaptchaDetectionPingUtils.i32SafeDate() - lazy.submissionInterval
    ) {
      lazy.console.debug("Not enough time has passed since last submission.");
      return;
    }

    CaptchaDetectionPingUtils.flushPing();
  }

  static prefsOfInterest = {
    networkCookieCookiebehavior: {
      type: "Int",
      name: "network.cookie.cookieBehavior",
    },
    privacyTrackingprotectionEnabled: {
      type: "Bool",
      name: "privacy.trackingprotection.enabled",
    },
    privacyTrackingprotectionCryptominingEnabled: {
      type: "Bool",
      name: "privacy.trackingprotection.cryptomining.enabled",
    },
    privacyTrackingprotectionFingerprintingEnabled: {
      type: "Bool",
      name: "privacy.trackingprotection.fingerprinting.enabled",
    },
    privacyFingerprintingprotection: {
      type: "Bool",
      name: "privacy.fingerprintingProtection",
    },
    networkCookieCookiebehaviorOptinpartitioning: {
      type: "Bool",
      name: "network.cookie.cookieBehavior.optInPartitioning",
    },
    privacyResistfingerprinting: {
      type: "Bool",
      name: "privacy.resistFingerprinting",
    },
    privacyTrackingprotectionPbmEnabled: {
      type: "Bool",
      name: "privacy.trackingprotection.pbmode.enabled",
    },
    privacyFingerprintingprotectionPbm: {
      type: "Bool",
      name: "privacy.fingerprintingProtection.pbmode",
    },
    networkCookieCookiebehaviorOptinpartitioningPbm: {
      type: "Bool",
      name: "network.cookie.cookieBehavior.optInPartitioning.pbmode",
    },
    privacyResistfingerprintingPbmode: {
      type: "Bool",
      name: "privacy.resistFingerprinting.pbmode",
    },
  };

  static initialized = false;
  static profileIsOpen = true;

  static init() {
    if (CaptchaDetectionPingUtils.initialized) {
      return;
    }

    Object.values(CaptchaDetectionPingUtils.prefsOfInterest).forEach(pref => {
      Services.prefs.addObserver(
        pref.name,
        CaptchaDetectionPingUtils.flushPing
      );
    });

    // maybeSubmitPing changes lastSubmission, and causes tests to fail.
    if (!Cu.isInAutomation) {
      ChromeUtils.idleDispatch(() =>
        CaptchaDetectionPingUtils.maybeSubmitPing(false)
      );
    }

    this.initialized = true;
    try {
      lazy.AsyncShutdown.profileBeforeChange.addBlocker(
        "CaptchaDetectionPingUtils: Don't submit pings after shutdown",
        async () => {
          this.profileIsOpen = false;
        }
      );
    } catch (e) {
      // This is not a critical error, so we just log it.
      // According to https://searchfox.org/mozilla-central/rev/d5baa11e35e0186c3c867f4948010f0742198467/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl#82-103
      // this error can happen if it is too late to add a blocker.
      lazy.console.error(
        "Failed to add blocker for profileBeforeChange: " + e.message
      );
      this.profileIsOpen = false;
    }
  }
}

/**
 * @typedef lazy
 * @type {object}
 * @property {ConsoleInstance} console - console instance.
 * @property {AsyncShutdown} AsyncShutdown - AsyncShutdown module.
 */
