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

const PREF_LOGLEVEL = "browser.policies.loglevel";

const lazy = {};

ChromeUtils.defineLazyGetter(lazy, "log", () => {
  let { ConsoleAPI } = ChromeUtils.importESModule(
    "resource://gre/modules/Console.sys.mjs"
  );
  return new ConsoleAPI({
    prefix: "macOSPoliciesParser",
    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
    // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
    maxLogLevel: "error",
    maxLogLevelPref: PREF_LOGLEVEL,
  });
});

export var macOSPoliciesParser = {
  readPolicies(reader) {
    let nativePolicies = reader.readPreferences();
    if (!nativePolicies) {
      return null;
    }

    nativePolicies = this.unflatten(nativePolicies);
    nativePolicies = this.removeUnknownPolicies(nativePolicies);

    // Need an extra check here so we don't
    // JSON.stringify if we aren't in debug mode
    if (lazy.log.maxLogLevel == "debug") {
      lazy.log.debug(JSON.stringify(nativePolicies, null, 2));
    }

    return nativePolicies;
  },

  removeUnknownPolicies(policies) {
    let { schema } = ChromeUtils.importESModule(
      "resource:///modules/policies/schema.sys.mjs"
    );

    for (let policyName of Object.keys(policies)) {
      if (!schema.properties.hasOwnProperty(policyName)) {
        lazy.log.debug(`Removing unknown policy: ${policyName}`);
        delete policies[policyName];
      }
    }

    return policies;
  },

  unflatten(input, delimiter = "__") {
    let ret = {};

    for (let key of Object.keys(input)) {
      if (!key.includes(delimiter)) {
        // Short-circuit for policies that are not specified in
        // the flat format.
        ret[key] = input[key];
        continue;
      }

      lazy.log.debug(`Unflattening policy key "${key}".`);

      let subkeys = key.split(delimiter);

      // `obj`: is the intermediate step into the unflattened
      // return object. For example, for an input:
      //
      // Foo__Bar__Baz: 5,
      //
      // when the subkey being iterated is Bar, then `obj` will be
      // the Bar object being constructed, as represented below:
      //
      // ret = {
      //   Foo = {
      //     Bar = {   <---- obj
      //       Baz: 5,
      //     }
      //   }
      // }
      let obj = ret;

      // Iterate until the second to last subkey, as the last one
      // needs special handling afterwards.
      for (let i = 0; i < subkeys.length - 1; i++) {
        let subkey = subkeys[i];

        if (!isValidSubkey(subkey)) {
          lazy.log.error(
            `Error in key ${key}: can't use indexes bigger than 50.`
          );
          continue;
        }

        if (!obj[subkey]) {
          // if this subkey hasn't been seen yet, create the object
          // for it, which could be an array if the next subkey is
          // a number.
          //
          // For example, in the following examples:
          // A)
          // Foo__Bar__0
          // Foo__Bar__1
          //
          // B)
          // Foo__Bar__Baz
          // Foo__Bar__Qux
          //
          // If the subkey being analysed right now is Bar, then in example A
          // we'll create an array to accomodate the numeric entries.
          // Otherwise, if it's example B, we'll create an object to host all
          // the named keys.
          if (Number.isInteger(Number(subkeys[i + 1]))) {
            obj[subkey] = [];
          } else {
            obj[subkey] = {};
          }
        }

        obj = obj[subkey];
      }

      let lastSubkey = subkeys[subkeys.length - 1];
      if (!isValidSubkey(lastSubkey)) {
        lazy.log.error(
          `Error in key ${key}: can't use indexes bigger than 50.`
        );
        continue;
      }

      // In the last subkey, we assign it the value by accessing the input
      // object again with the full key. For example, in the case:
      //
      // input = {"Foo__Bar__Baz": 5}
      //
      // what we're doing in practice is:
      //
      // ret["Foo"]["Bar"]["Baz"] = input["Foo__Bar__Baz"];
      // \_______ _______/   |
      //         v           |
      //        obj      last subkey

      obj[lastSubkey] = input[key];
    }

    return ret;
  },
};

function isValidSubkey(subkey) {
  let valueAsNumber = Number(subkey);
  return Number.isNaN(valueAsNumber) || valueAsNumber <= 50;
}
