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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
  EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
  Log: "resource://gre/modules/Log.sys.mjs",
});

// Define our prefs
const PREF_CLIENT_ID = "asanreporter.clientid";
const PREF_API_URL = "asanreporter.apiurl";
const PREF_AUTH_TOKEN = "asanreporter.authtoken";
const PREF_LOG_LEVEL = "asanreporter.loglevel";

// Reporter product map
const REPORTER_PRODUCT = {
  firefox: "mozilla-central-asan-nightly",
  thunderbird: "comm-central-asan-daily",
};

const LOGGER_NAME = "asanreporter";

let logger;

export const AsanReporter = {
  init() {
    if (this.initialized) {
      return;
    }
    this.initialized = true;

    // Setup logging
    logger = lazy.Log.repository.getLogger(LOGGER_NAME);
    logger.addAppender(
      new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
    );
    logger.addAppender(
      new lazy.Log.DumpAppender(new lazy.Log.BasicFormatter())
    );
    logger.level = Services.prefs.getIntPref(
      PREF_LOG_LEVEL,
      lazy.Log.Level.Info
    );

    logger.info("Starting up...");

    // Notify that ASan Reporter builds are not receiving any updates
    this.notifyDeprecation();

    // Install a handler to observe tab crashes, so we can report those right
    // after they happen instead of relying on the user to restart the browser.
    Services.obs.addObserver(this, "ipc:content-shutdown");

    processDirectory();
  },

  notifyDeprecation() {
    function showNotification(window) {
      // Eslint thinks DocumentFragment is available here but that's a lie.
      // eslint-disable-next-line no-shadow
      let { DocumentFragment, gNotificationBox, document } = window;
      let frag = new DocumentFragment();
      frag.append(
        "The ASAN Nightly project is being discontinued and this build " +
          "will soon receive "
      );
      let noUpdates = document.createElement("strong");
      noUpdates.append("no more updates");
      frag.append(noUpdates);
      frag.append(". Please switch to a supported build.");
      gNotificationBox.appendNotification(
        "asan-nightly-deprecation",
        {
          priority: gNotificationBox.PRIORITY_CRITICAL_HIGH,
          label: frag,
        },
        [
          {
            label: "Learn more\u2026",
            link: "https://firefox-source-docs.mozilla.org/tools/sanitizer/asan_nightly.html#deprecation-faq",
          },
        ],
        false,
        false
      );
    }
    lazy.EveryWindow.registerCallback(
      "asan-reporter",
      showNotification,
      // We have to provide an uninit function to avoid EveryWindow complaining.
      // But we don't actually need to do anything:
      () => {},
      false // don't care about clickjacking the learn more link.
    );
  },

  observe(aSubject, aTopic) {
    if (aTopic == "ipc:content-shutdown") {
      aSubject.QueryInterface(Ci.nsIPropertyBag2);
      if (!aSubject.get("abnormal")) {
        return;
      }
      processDirectory();
    }
  },
};

async function processDirectory() {
  const asanDumpDir = PathUtils.join(PathUtils.profileDir, "asan");
  const children = await IOUtils.getChildren(asanDumpDir);

  const results = children.filter(function (path) {
    const name = PathUtils.filename(path);
    return name.startsWith("ff_asan_log.") && !name.includes("submitted");
  });

  logger.info(`Processing ${results.length} reports...`);
  for (const result of results) {
    try {
      await submitReport(result);
      logger.info(`Successfully submitted ${result.path}`);
    } catch (e) {
      logger.error(`Failed to submit ${result.path}. Reason: ${e}`);
    }
  }

  logger.info("Done processing reports.");
}

async function submitReport(reportFile) {
  logger.info("Processing " + reportFile);
  const data = await IOUtils.read(reportFile);
  await submitToServer(data);
  // Mark as submitted only if we successfully submitted it to the server.
  await IOUtils.move(reportFile, `${reportFile}.submitted`);
}

function submitToServer(data) {
  return new Promise(function (resolve, reject) {
    logger.debug("Setting up XHR request");
    let client = Services.prefs.getStringPref(PREF_CLIENT_ID);
    let api_url = Services.prefs.getStringPref(PREF_API_URL);
    let auth_token = Services.prefs.getStringPref(PREF_AUTH_TOKEN, null);

    let decoder = new TextDecoder();

    if (!client) {
      client = "unknown";
    }

    let versionArr = [
      Services.appinfo.version,
      Services.appinfo.appBuildID,
      AppConstants.SOURCE_REVISION_URL || "unknown",
    ];

    // Concatenate all relevant information as our server only
    // has one field available for version information.
    let product_version = versionArr.join("-");
    let os = AppConstants.platform;
    let reporter_product = REPORTER_PRODUCT[AppConstants.MOZ_APP_NAME];

    let reportObj = {
      rawStdout: "",
      rawStderr: "",
      rawCrashData: decoder.decode(data),
      // Hardcode platform as there is no other reasonable platform for ASan
      platform: "x86-64",
      product: reporter_product,
      product_version,
      os,
      client,
      tool: "asan-nightly-program",
    };

    var xhr = new XMLHttpRequest({ mozAnon: !auth_token });
    xhr.open("POST", api_url, true);
    xhr.setRequestHeader("Content-Type", "application/json");

    // For internal testing purposes, an auth_token can be specified
    if (auth_token) {
      xhr.setRequestHeader("Authorization", "Token " + auth_token);
    } else {
      // Prevent privacy leaks
      xhr.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
    }

    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4) {
        if (xhr.status == "201") {
          logger.debug("XHR: OK");
          resolve(xhr);
        } else {
          logger.debug(
            "XHR: Status: " + xhr.status + " Response: " + xhr.responseText
          );
          reject(xhr);
        }
      }
    };

    xhr.send(JSON.stringify(reportObj));
  });
}
