 makeConfigProperty({
    obj,
    propName,
    defaultVal,
    prefName,
    isProviderList,
  }) {
    let prefFn = isProviderList
      ? getProviderListFromPref
      : getPrefValueRegionFirst;

    let overridePropName = "_" + propName;

    Object.defineProperty(obj, propName, {
      get() {
        // If a pref value exists, it gets top priority. Otherwise, if it has an
        // explicitly set value (from Remote Settings), we return that.
        let prefVal = prefFn(prefName);
        if (prefVal !== undefined) {
          return prefVal;
        }
        if (this[overridePropName] !== undefined) {
          return this[overridePropName];
        }
        return defaultVal;
      },
      set(val) {
        this[overridePropName] = val;
      },
    });
  }
  let newConfig = {
    get fallbackProviderURI() {
      return this.providerList[0]?.uri;
    },
    trrSelection: {},
    providerSteering: {},
  };
  makeConfigProperty({
    obj: newConfig,
    propName: "enabled",
    defaultVal: false,
    prefName: kConfigPrefs.kEnabledPref,
    isProviderList: false,
  });
  makeConfigProperty({
    obj: newConfig,
    propName: "providerList",
    defaultVal: [],
    prefName: kConfigPrefs.kProvidersPref,
    isProviderList: true,
  });
  makeConfigProperty({
    obj: newConfig.trrSelection,
    propName: "enabled",
    defaultVal: false,
    prefName: kConfigPrefs.kTRRSelectionEnabledPref,
    isProviderList: false,
  });
  makeConfigProperty({
    obj: newConfig.trrSelection,
    propName: "commitResult",
    defaultVal: false,
    prefName: kConfigPrefs.kTRRSelectionCommitResultPref,
    isProviderList: false,
  });
  makeConfigProperty({
    obj: newConfig.trrSelection,
    propName: "providerList",
    defaultVal: [],
    prefName: kConfigPrefs.kTRRSelectionProvidersPref,
    isProviderList: true,
  });
  makeConfigProperty({
    obj: newConfig.providerSteering,
    propName: "enabled",
    defaultVal: false,
    prefName: kConfigPrefs.kProviderSteeringEnabledPref,
    isProviderList: false,
  });
  makeConfigProperty({
    obj: newConfig.providerSteering,
    propName: "providerList",
    defaultVal: [],
    prefName: kConfigPrefs.kProviderSteeringListPref,
    isProviderList: true,
  });
  return newConfig;
}

export const DoHConfigController = {
  initComplete: null,
  _resolveInitComplete: null,

  // This field always contains the current config state, for
  // consumer use.
  currentConfig: makeBaseConfigObject(),

  // Loads the client's region via Region.sys.mjs. This might mean waiting
  // until the region is available.
  async loadRegion() {
    await new Promise(resolve => {
      // If the region has changed since it was last set, update the pref.
      let homeRegionChanged = lazy.Preferences.get(
        `${kGlobalPrefBranch}.home-region-changed`
      );
      if (homeRegionChanged) {
        lazy.Preferences.reset(`${kGlobalPrefBranch}.home-region-changed`);
        lazy.Preferences.reset(`${kGlobalPrefBranch}.home-region`);
      }

      let homeRegion = lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`);
      if (homeRegion) {
        resolve();
        return;
      }

      let updateRegionAndResolve = () => {
        lazy.Preferences.set(
          `${kGlobalPrefBranch}.home-region`,
          currentRegion()
        );
        resolve();
      };

      if (currentRegion()) {
        updateRegionAndResolve();
        return;
      }

      Services.obs.addObserver(function obs() {
        Services.obs.removeObserver(obs, lazy.Region.REGION_TOPIC);
        updateRegionAndResolve();
      }, lazy.Region.REGION_TOPIC);
    });

    // Finally, reload config.
    await this.updateFromRemoteSettings();
  },

  async init() {
    await this.loadRegion();

    Services.prefs.addObserver(`${kGlobalPrefBranch}.`, this, true);
    Services.obs.addObserver(this, "idle-daily", true);
    Services.obs.addObserver(this, "default-timezone-changed", true);

    gProvidersCollection.on("sync", this.updateFromRemoteSettings);
    gConfigCollection.on("sync", this.updateFromRemoteSettings);

    this._resolveInitComplete();
  },

  // Useful for tests to set prior state before init()
  async _uninit() {
    await this.initComplete;

    Services.prefs.removeObserver(`${kGlobalPrefBranch}`, this);
    Services.obs.removeObserver(this, "idle-daily");
    Services.obs.removeObserver(this, "default-timezone-changed");

    gProvidersCollection.off("sync", this.updateFromRemoteSettings);
    gConfigCollection.off("sync", this.updateFromRemoteSettings);

    this.initComplete = new Promise(resolve => {
      this._resolveInitComplete = resolve;
    });
  },

  // Performs a region check when the timezone changes
  async getRegionAndNotify() {
    await lazy.Region._fetchRegion();
    if (
      currentRegion() &&
      currentRegion() !=
        lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`)
    ) {
      lazy.Preferences.set(`${kGlobalPrefBranch}.home-region`, currentRegion());
      this.notifyNewConfig();
    }
  },

  observe(subject, topic, data) {
    switch (topic) {
      case kPrefChangedTopic:
        {
          let allowedPrefs = Object.getOwnPropertyNames(kConfigPrefs).map(
            k => kConfigPrefs[k]
          );
          if (
            !allowedPrefs.some(pref =>
              [
                `${regionPrefBranch()}.${pref}`,
                `${kGlobalPrefBranch}.${pref}`,
              ].includes(data)
            )
          ) {
            break;
          }
          this.notifyNewConfig();
        }
        break;
      case "idle-daily":
        if (
          currentRegion() &&
          currentRegion() !=
            lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`)
        ) {
          lazy.Preferences.set(
            `${kGlobalPrefBranch}.home-region`,
            currentRegion()
          );
          this.notifyNewConfig();
        }
        break;
      case "default-timezone-changed":
        this.getRegionAndNotify();
        break;
    }
  },

  QueryInterface: ChromeUtils.generateQI([
    "nsIObserver",
    "nsISupportsWeakReference",
  ]),

  // Creates new config object from currently available
  // Remote Settings values.
  async updateFromRemoteSettings() {
    let providers = await gProvidersCollection.get();
    let config = await gConfigCollection.get();

    let providersById = new Map();
    providers.forEach(p => providersById.set(p.id, p));

    let configByRegion = new Map();
    config.forEach(c => {
      c.id = c.id.toLowerCase();
      configByRegion.set(c.id, c);
    });

    let homeRegion = lazy.Preferences.get(`${kGlobalPrefBranch}.home-region`);
    let localConfig =
      configByRegion.get(homeRegion?.toLowerCase()) ||
      configByRegion.get("global");

    // Make a new config object first, mutate it as needed, then synchronously
    // replace the currentConfig object at the end to ensure atomicity.
    let newConfig = makeBaseConfigObject();

    if (!localConfig) {
      DoHConfigController.currentConfig = newConfig;
      DoHConfigController.notifyNewConfig();
      return;
    }

    let isAndroid = Services.appinfo.OS === "Android";
    if (
      (isAndroid && localConfig.androidRolloutEnabled) ||
      (!isAndroid && localConfig.rolloutEnabled)
    ) {
      newConfig.enabled = true;
    }

    let parseProviderList = (list, checkFn) => {
      let parsedList = [];
      list?.split(",")?.forEach(p => {
        p = p.trim();
        if (!p.length) {
          return;
        }
        p = providersById.get(p);
        if (!p || (checkFn && !checkFn(p))) {
          return;
        }
        parsedList.push(p);
      });
      return parsedList;
    };

    let regionalProviders = parseProviderList(localConfig.providers);
    if (regionalProviders?.length) {
      newConfig.providerList = regionalProviders;
    }

    if (localConfig.steeringEnabled) {
      let steeringProviders = parseProviderList(
        localConfig.steeringProviders,
        p => p.canonicalName?.length
      );
      if (steeringProviders?.length) {
        newConfig.providerSteering.providerList = steeringProviders;
        newConfig.providerSteering.enabled = true;
      }
    }

    if (localConfig.autoDefaultEnabled) {
      let defaultProviders = parseProviderList(
        localConfig.autoDefaultProviders
      );
      if (defaultProviders?.length) {
        newConfig.trrSelection.providerList = defaultProviders;
        newConfig.trrSelection.enabled = true;
      }
    }

    // Finally, update the currentConfig object synchronously.
    DoHConfigController.currentConfig = newConfig;

    DoHConfigController.notifyNewConfig();
  },

  kConfigUpdateTopic: "doh-config-updated",
  notifyNewConfig() {
    Services.obs.notifyObservers(null, this.kConfigUpdateTopic);
  },
};

DoHConfigController.initComplete = new Promise(resolve => {
  DoHConfigController._resolveInitComplete = resolve;
});
DoHConfigController.init();
PK