lib: let
  importers = import ./importers.nix lib;

  inherit (importers) flattenTree rakeLeaves;
in rec {
  /*
  Generate an attrset by mapping a function over a list of attribute values.

  Do not confuse this with `genAttrs` from nixpkgs; this function applies `f`
  to the value, rather than the name.

  @param  values list of values in the resultant attrset
  @param  f      function that creates a name for the attribute
  @return        attrset with generated names mapped to provided values
  */
  genAttrs' = values: f: with lib; listToAttrs (map (v: nameValuePair (f v) v) values);

  /*
  Generate an enable option for a Nix module file with a given path; all
  generated enable options are `false` by default.

  This implementation is rather clunky, because the `imports` list needs to
  be evaluated first before evaluating other options. Thus, it cannot be used
  inside a `config` attribute that needs to be evaluated later, and one needs
  to pass all of the arguments that the `imports` list would otherwise pass
  manually.

  @param  args   arguments attrset to pass to module
  @param  path   path to generated option inside an attset
  @param  module Nix module to generate enable option for
  @return        module with an enable option to import the given Nix module
  */
  genProfileOption = with lib;
    args: path: module: let
      components = splitString "." path;
      opt = components ++ ["enable"];
    in
      {config, ...}: {
        # Option for given module; generate an option in a nested attrset
        options = lib.setAttrByPath opt (mkOption {
          type = types.bool;
          default = false;
          description = "Whether to enable ${last components}";
        });

        # Import the module and ass it the arguments given; the
        # resultant attrset is a valid `config`.
        config =
          mkIf (getAttrFromPath opt config)
          (import module args);
      };

  /*
  Generate an enable option for a group of Nix files within a directory.

  This should be used in tandem with `genProfileOption`; otherwise it will
  generate non-existent enable options.

  @param  group path of group options inside an attrset
  @param  tree  structure of all profile options generated by rakeLeaves
  @return       module with an enable option that enables all of the enable
                options generated with `genProfileOption within a directory
  */
  genGroupOption = with lib;
    group: tree: let
      path = splitString "." group;
      opt = path ++ ["enable"];

      # The prefix is invalid when looking up attrs in `tree`.
      pathWithoutPrefix = drop 1 path;
    in
      {config, ...}: {
        # Option for given module; generate an option in a nested attrset
        options = setAttrByPath opt (mkOption {
          type = types.bool;
          default = false;
          description = "Whether to enable ${last path}";
        });

        # Set all module enable options inside the path to `true` with
        # mkDefault priority.
        config =
          mkIf (getAttrFromPath opt config)
          # Attr at `path` will be an attrset with all the enable options
          (setAttrByPath path
            # New attrset with all of the enable options set
            (mapAttrs
              (_: _: {enable = mkDefault true;})
              (getAttrFromPath pathWithoutPrefix tree)));
      };

  /*
  Generate both module and group options for a given directory that contains
  Nix modules.

  The modules can be arbitrarily strutcured; group options will be generated
  for directories within `directory` that contain modules and that don't have
  a `default.nix` file in them, due to the nature of `rakeLeaves`.

  @param  args      arguments attrset to pass to all modules
  @param  prefix    prefix to append to all module options
  @param  directory directory that contains Nix modules to generate options for
  @return           list of modules with enable options for all modules/groups in a directory
  */
  genModules = with lib;
    args: prefix: directory: let
      # Tree with modules and their paths in attrset form
      tree = rakeLeaves directory;
      # Contains paths for each profile in string form
      leaves' = flattenTree tree;
      # Contains paths for each profile with the prefix added
      leaves = mapAttrs' (n: nameValuePair "${prefix}.${n}") leaves';

      # Remove a module's name from a path; used when generating group names
      removeModuleName = name:
        with lib; let
          components = splitString "." name;
        in
          concatStringsSep "."
          (take ((length components) - 1) components);

      # List of group names, generated from all profile names without the prefix
      groupNames =
        map
        (n: "${prefix}.${n}")
        # Duplicates exist due to stripping profile names
        (unique
          (mapAttrsToList
            (n: _: removeModuleName n)
            # Only use profile names with a "." in them to ensure
            # group names aren't empty
            (filterAttrs (n: _: hasInfix "." n) leaves')));
    in
      mapAttrsToList (genProfileOption args) leaves # Generated profile options
      ++ (map (n: genGroupOption n tree) groupNames); # Generated group options

  /*
  Generate system configuration profiles for all provided configurations with
  a given builder function.

  A builder takes a hostname and a configuration, along with other options, and
  creates a system derivation function with some default configurations and
  imports. The architectures of the resultant systems need to be filled in, as
  well as any other options if provided.

  @param  builder function that takes a hostname and configuration and returns a system derivation
  @param  hosts   directory that contains hosts and their configurations
  @return         attrset of hosts
  */
  genHosts = builder: hosts: with lib; mapAttrs builder (rakeLeaves hosts);
}
