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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ProxyTypes: "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "protocolProxyService",
  "@mozilla.org/network/protocol-proxy-service;1",
  Ci.nsIProtocolProxyService
);

const nsIProtocolProxyChannelFilter = ChromeUtils.generateQI([
  Ci.nsIProtocolProxyChannelFilter,
]);

// The maximum uint32 value.
const PR_UINT32_MAX = 4294967295;

/**
 * A ProxyPerUserContextManager class keeps track of user contexts and their proxy configuration.
 */
export class ProxyPerUserContextManager {
  #channelProxyFilter;
  #proxyFilterRegistered;
  #userContextToProxyConfiguration;

  constructor() {
    this.#proxyFilterRegistered = false;

    // A map between internal user context ids and proxy configurations.
    this.#userContextToProxyConfiguration = new Map();
  }

  destroy() {
    this.#userContextToProxyConfiguration = new Map();

    this.#unregisterProxyFilter();
  }

  /**
   * Add proxy configuration for a provided user context id.
   *
   * @param {string} userContextId
   *     Internal user context id.
   * @param {Proxy} proxy
   *     Proxy configuration.
   */
  addConfiguration(userContextId, proxy) {
    this.#userContextToProxyConfiguration.set(userContextId, proxy);

    this.#registerProxyFilter();
  }

  /**
   * Delete proxy configuration for a provided user context id.
   *
   * @param {string} userContextId
   *     Internal user context id.
   */
  deleteConfiguration(userContextId) {
    this.#userContextToProxyConfiguration.delete(userContextId);

    this.#unregisterProxyFilter();
  }

  #addProxyFilter(proxyFilter, proxySettings) {
    const { host, port, type } = proxySettings;

    proxyFilter.onProxyFilterResult(
      lazy.protocolProxyService.newProxyInfo(
        type,
        host,
        port,
        "" /* aProxyAuthorizationHeader */,
        "" /* aConnectionIsolationKey */,
        Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST /* aFlags */,
        PR_UINT32_MAX /* aFailoverTimeout */,
        null /* failover proxy */
      )
    );
  }

  #applyFilter = (channel, defaultProxyInfo, proxyFilter) => {
    const originAttributes =
      channel.loadInfo && channel.loadInfo.originAttributes;

    if (
      this.#userContextToProxyConfiguration.has(originAttributes.userContextId)
    ) {
      const proxyInfo = this.#userContextToProxyConfiguration.get(
        originAttributes.userContextId
      );

      if (proxyInfo.proxyType === lazy.ProxyTypes.Direct) {
        proxyFilter.onProxyFilterResult(null);

        return;
      }

      if (proxyInfo.proxyType === lazy.ProxyTypes.Manual) {
        const channelURI = channel.originalURI;
        for (const url of proxyInfo.noProxy ?? []) {
          if (
            (url.startsWith(".") && channelURI.host.endsWith(url)) ||
            (!url.startsWith(".") && channelURI.host === url)
          ) {
            proxyFilter.onProxyFilterResult(defaultProxyInfo);

            // If at least one element from noProxy matches the channel URL no need to check further.
            return;
          }
        }

        if (proxyInfo.httpProxy) {
          this.#addProxyFilter(proxyFilter, {
            host: proxyInfo.httpProxy,
            port: proxyInfo.httpProxyPort,
            type: "http",
          });
        }

        if (proxyInfo.sslProxy) {
          this.#addProxyFilter(proxyFilter, {
            host: proxyInfo.sslProxy,
            port: proxyInfo.sslProxyPort,
            type: "https",
          });
        }

        if (proxyInfo.socksProxy) {
          this.#addProxyFilter(proxyFilter, {
            host: proxyInfo.socksProxy,
            port: proxyInfo.socksProxyPort,
            type:
              proxyInfo.socksVersion === 5
                ? "socks"
                : `socks${proxyInfo.socksVersion}`,
          });
        }

        return;
      }
    }

    proxyFilter.onProxyFilterResult(defaultProxyInfo);
  };

  #registerProxyFilter() {
    if (!this.#proxyFilterRegistered) {
      this.#proxyFilterRegistered = true;

      this.#channelProxyFilter = {
        QueryInterface: nsIProtocolProxyChannelFilter,
        applyFilter: this.#applyFilter,
      };

      lazy.protocolProxyService.registerChannelFilter(
        this.#channelProxyFilter,
        0 /* set position `0` to override global filters */
      );
    }
  }

  #unregisterProxyFilter() {
    if (
      this.#proxyFilterRegistered &&
      this.#userContextToProxyConfiguration.size === 0
    ) {
      this.#proxyFilterRegistered = false;
      lazy.protocolProxyService.unregisterChannelFilter(
        this.#channelProxyFilter
      );
      this.#channelProxyFilter = null;
    }
  }
}
