/* 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/. */

/**
 * Service Class to observe and record proxy-errors related to IP-Protection
 *
 * @fires event:"proxy-http-error"
 * Fired when the Proxy has recieved the Connect Request and responded with
 * a non-2xx HTTP status code
 */
export class IPPNetworkErrorObserver {
  constructor() {}

  start() {
    if (this.#active) {
      return;
    }
    Services.obs.addObserver(this, "http-on-stop-request");
    Services.obs.addObserver(this, "http-on-failed-opening-request");
    this.#active = true;
  }
  stop() {
    if (!this.#active) {
      return;
    }
    this.#active = false;
    this.#isolationKeys.clear();
    Services.obs.removeObserver(this, "http-on-failed-opening-request");
    Services.obs.removeObserver(this, "http-on-stop-request");
  }

  addIsolationKey(key) {
    if (typeof key !== "string" || !key) {
      throw new Error("Isolation key must be a non-empty string");
    }
    this.#isolationKeys.add(key);
  }
  removeIsolationKey(key) {
    if (typeof key !== "string" || !key) {
      throw new Error("Isolation key must be a non-empty string");
    }
    this.#isolationKeys.delete(key);
  }
  addEventListener(...args) {
    this._event.addEventListener(...args);
  }

  removeEventListener(...args) {
    this._event.removeEventListener(...args);
  }

  observe(subject, topic, _data) {
    if (
      topic !== "http-on-stop-request" &&
      topic !== "http-on-failed-opening-request"
    ) {
      return;
    }
    try {
      const chan = subject.QueryInterface(Ci.nsIHttpChannel);
      const key = this.getKey(chan);
      if (!key) {
        // If the isolation key is unknown to us or does not
        // exist, no need to care.
        return;
      }
      const proxiedChannel = chan.QueryInterface(Ci.nsIProxiedChannel);
      const proxycode = proxiedChannel.httpProxyConnectResponseCode;
      switch (proxycode) {
        case 0:
        case 200:
          // All good :)
          return;
        default:
          this.#emitProxyHTTPError(this.#classifyLoad(chan), key, proxycode);
      }
    } catch (err) {
      // If the channel is not an nsIHttpChannel or not proxied - all good.
    }
  }
  /**
   * Checks if a channel should be counted.
   *
   * @param {nsIHttpChannel} channel
   * @returns {boolean} true if the channel should be counted.
   */
  getKey(channel) {
    try {
      const proxiedChannel = channel.QueryInterface(Ci.nsIProxiedChannel);
      const proxyInfo = proxiedChannel.proxyInfo;
      if (!proxyInfo) {
        // No proxy info, nothing to do.
        return null;
      }
      const isolationKey = proxyInfo.connectionIsolationKey;
      if (!isolationKey || !this.#isolationKeys.has(isolationKey)) {
        return null;
      }
      return isolationKey;
    } catch (err) {
      // If the channel is not an nsIHttpChannel or nsIProxiedChannel, as it's irrelevant
      // for this class.
    }
    return null;
  }

  #classifyLoad(channel) {
    try {
      if (channel.isMainDocumentChannel) {
        return "error";
      }
      return "warning";
    } catch (_) {}
    return "unknown";
  }

  #emitProxyHTTPError(level, isolationKey, httpStatus) {
    this._event.dispatchEvent(
      new CustomEvent("proxy-http-error", {
        detail: { level, isolationKey, httpStatus },
      })
    );
  }
  _event = new EventTarget();

  #active = false;
  #isolationKeys = new Set();
}

IPPNetworkErrorObserver.prototype.QueryInterface = ChromeUtils.generateQI([
  Ci.nsIObserver,
]);
