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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  CommonUtils: "resource://services-common/utils.sys.mjs",
  ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
});

const COVERAGE_VERSION = "2";

const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled";
const LOG_LEVEL_PREF = "toolkit.coverage.log-level";
const OPT_OUT_PREF = "toolkit.coverage.opt-out";
const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`;
const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`;
const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
const REPORTING_ENDPOINT_BASE_PREF = `toolkit.coverage.endpoint.base`;
const REPORTING_ENDPOINT = "submit/coverage/coverage";
const PING_SUBMISSION_TIMEOUT = 30 * 1000; // 30 seconds

const log = Log.repository.getLogger("Telemetry::CoveragePing");
log.level = Services.prefs.getIntPref(LOG_LEVEL_PREF, Log.Level.Error);
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));

export var CoveragePing = Object.freeze({
  async startup() {
    if (!Services.prefs.getBoolPref(COVERAGE_ENABLED_PREF, false)) {
      log.debug("coverage not enabled");
      return;
    }

    if (Services.prefs.getBoolPref(OPT_OUT_PREF, false)) {
      log.debug("user has set opt-out pref");
      return;
    }

    if (Services.prefs.getBoolPref(ALREADY_RUN_PREF, false)) {
      log.debug("already run on this profile");
      return;
    }

    if (!Services.prefs.getCharPref(REPORTING_ENDPOINT_BASE_PREF, null)) {
      log.error("no endpoint base set");
      return;
    }

    try {
      await this.reportTelemetrySetting();
    } catch (e) {
      log.error("unable to upload payload", e);
    }
  },

  // NOTE - this does not use existing Telemetry code or honor Telemetry opt-out prefs,
  // by design. It also sends no identifying data like the client ID. See the "coverage ping"
  // documentation for details.
  reportTelemetrySetting() {
    const enabled = Services.prefs.getBoolPref(TELEMETRY_ENABLED_PREF, false);

    const payload = {
      appVersion: Services.appinfo.version,
      appUpdateChannel: lazy.UpdateUtils.getUpdateChannel(false),
      osName: Services.sysinfo.getProperty("name"),
      osVersion: Services.sysinfo.getProperty("version"),
      telemetryEnabled: enabled,
    };

    let cachedUuid = Services.prefs.getCharPref(COVERAGE_UUID_PREF, null);
    if (!cachedUuid) {
      // Totally random UUID, just for detecting duplicates.
      cachedUuid = lazy.CommonUtils.generateUUID();
      Services.prefs.setCharPref(COVERAGE_UUID_PREF, cachedUuid);
    }

    let reportingEndpointBase = Services.prefs.getCharPref(
      REPORTING_ENDPOINT_BASE_PREF,
      null
    );

    let endpoint = `${reportingEndpointBase}/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${cachedUuid}`;

    log.debug(`putting to endpoint ${endpoint} with payload:`, payload);

    let deferred = Promise.withResolvers();

    let request = new lazy.ServiceRequest({ mozAnon: true });
    request.mozBackgroundRequest = true;
    request.timeout = PING_SUBMISSION_TIMEOUT;

    request.open("PUT", endpoint, true);
    request.overrideMimeType("text/plain");
    request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
    request.setRequestHeader("Date", new Date().toUTCString());

    let errorhandler = event => {
      let failure = event.type;
      log.error(`error making request to ${endpoint}: ${failure}`);
      deferred.reject(event);
    };

    request.onerror = errorhandler;
    request.ontimeout = errorhandler;
    request.onabort = errorhandler;

    request.onloadend = event => {
      let status = request.status;
      let statusClass = status - (status % 100);
      let success = false;

      if (statusClass === 200) {
        // We can treat all 2XX as success.
        log.info(`successfully submitted, status: ${status}`);
        success = true;
      } else if (statusClass === 400) {
        // 4XX means that something with the request was broken.

        // TODO: we should handle this better, but for now we should avoid resubmitting
        // broken requests by pretending success.
        success = true;
        log.error(
          `error submitting to ${endpoint}, status: ${status} - ping request broken?`
        );
      } else if (statusClass === 500) {
        // 5XX means there was a server-side error and we should try again later.
        log.error(
          `error submitting to ${endpoint}, status: ${status} - server error, should retry later`
        );
      } else {
        // We received an unexpected status code.
        log.error(
          `error submitting to ${endpoint}, status: ${status}, type: ${event.type}`
        );
      }

      if (success) {
        Services.prefs.setBoolPref(ALREADY_RUN_PREF, true);
        log.debug(`result from PUT: ${request.responseText}`);
        deferred.resolve();
      } else {
        deferred.reject(event);
      }
    };

    request.send(JSON.stringify(payload));

    return deferred.promise;
  },
});
