{
  modulePath,
  name,
  description ? null,
  wrappedPackageName ? null,
  unwrappedPackageName ? null,
  platforms,
  enableBookmarks ? true,
}:
{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    attrValues
    concatStringsSep
    length
    literalExpression
    mapAttrsToList
    mkIf
    mkMerge
    mkOption
    mkOptionDefault
    optionalString
    optional
    setAttrByPath
    types
    ;
  inherit (pkgs.stdenv.hostPlatform) isDarwin;

  appName = name;

  moduleName = concatStringsSep "." modulePath;

  cfg = lib.getAttrFromPath modulePath config;

  jsonFormat = pkgs.formats.json { };

  supportedPlatforms = lib.flatten (lib.attrVals (lib.attrNames platforms) lib.platforms);

  isWrapped = lib.versionAtLeast config.home.stateVersion "19.09" && wrappedPackageName != null;

  defaultPackageName = if isWrapped then wrappedPackageName else unwrappedPackageName;

  packageName = if wrappedPackageName != null then wrappedPackageName else unwrappedPackageName;

  # The extensions path shared by all profiles; will not be supported
  # by future browser versions.
  extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";

  profiles =
    lib.flip lib.mapAttrs' cfg.profiles (
      _: profile:
      lib.nameValuePair "Profile${toString profile.id}" {
        Name = profile.name;
        Path = if isDarwin then "Profiles/${profile.path}" else profile.path;
        IsRelative = 1;
        Default = if profile.isDefault then 1 else 0;
      }
    )
    // {
      General = {
        StartWithLastProfile = 1;
      }
      // lib.optionalAttrs (cfg.profileVersion != null) {
        Version = cfg.profileVersion;
      };
    };

  profilesIni = lib.generators.toINI { } profiles;

  userPrefValue =
    pref:
    builtins.toJSON (
      if lib.isBool pref || lib.isInt pref || lib.isString pref || lib.isPath pref then
        pref
      else
        builtins.toJSON pref
    );

  extensionSettingsNeedForce =
    extensionSettings: builtins.any (ext: ext.settings != { }) (attrValues extensionSettings);

  extensionSettingsMissingForce =
    extensionSettings:
    builtins.any (ext: ext.settings != { } && !ext.force) (attrValues extensionSettings);

  mkUserJs =
    prePrefs: prefs: extraPrefs: bookmarksFile: extensions:
    let
      prefs' =
        lib.optionalAttrs (bookmarksFile != null) {
          "browser.bookmarks.file" = toString bookmarksFile;
          "browser.places.importBookmarksHTML" = true;
        }
        // lib.optionalAttrs (extensionSettingsNeedForce extensions) {
          "extensions.webextensions.ExtensionStorageIDB.enabled" = false;
        }
        // prefs;
    in
    ''
      // Generated by Home Manager.

      ${prePrefs}

      ${lib.concatStrings (
        mapAttrsToList (name: value: ''
          user_pref("${name}", ${userPrefValue value});
        '') prefs'
      )}

      ${extraPrefs}
    '';

  mkContainersJson =
    containers:
    let
      containerToIdentity = _: container: {
        userContextId = container.id;
        inherit (container) color icon name;
        public = true;
      };
    in
    ''
      ${builtins.toJSON {
        version = 5;
        lastUserContextId = lib.foldlAttrs (
          acc: _: value:
          if value.id > acc then value.id else acc
        ) 0 containers;
        identities = mapAttrsToList containerToIdentity containers ++ [
          {
            userContextId = 4294967294; # 2^32 - 2
            name = "userContextIdInternal.thumbnail";
            icon = "";
            color = "";
            accessKey = "";
            public = false;
          }
          {
            userContextId = 4294967295; # 2^32 - 1
            name = "userContextIdInternal.webextStorageLocal";
            icon = "";
            color = "";
            accessKey = "";
            public = false;
          }
        ];
      }}
    '';

  mkNoDuplicateAssertion =
    entities: entityKind:
    (
      let
        # Return an attribute set with entity IDs as keys and a list of
        # entity names with corresponding ID as value. An ID is present in
        # the result only if more than one entity has it. The argument
        # entities is a list of AttrSet of one id/name pair.
        findDuplicateIds =
          entities: lib.filterAttrs (_entityId: entityNames: length entityNames != 1) (lib.zipAttrs entities);

        duplicates = findDuplicateIds (
          mapAttrsToList (entityName: entity: { "${toString entity.id}" = entityName; }) entities
        );

        mkMsg = entityId: entityNames: "  - ID ${entityId} is used by " + concatStringsSep ", " entityNames;
      in
      {
        assertion = duplicates == { };
        message = ''
          Must not have a ${appName} ${entityKind} with an existing ID but
        ''
        + concatStringsSep "\n" (mapAttrsToList mkMsg duplicates);
      }
    );

  wrapPackage =
    package:
    let
      # The configuration expected by the Firefox wrapper.
      fcfg = {
        inherit (cfg) enableGnomeExtensions;
      };

      # A bit of hackery to force a config into the wrapper.
      browserName = package.browserName or (builtins.parseDrvName package.name).name;

      # The configuration expected by the Firefox wrapper builder.
      bcfg = setAttrByPath [ browserName ] fcfg;
    in
    if package == null then
      null
    else if isWrapped then
      package.override (old: {
        cfg = old.cfg or { } // fcfg;
        extraPolicies = (old.extraPolicies or { }) // cfg.policies;
        pkcs11Modules = (old.pkcs11Modules or [ ]) ++ cfg.pkcs11Modules;
      })
    else
      (pkgs.wrapFirefox.override { config = bcfg; }) package { };

  bookmarkTypes = import ./profiles/bookmark-types.nix { inherit lib; };
in
{
  options = setAttrByPath modulePath {
    enable = mkOption {
      type = types.bool;
      default = false;
      example = true;
      description = ''
        Whether to enable ${appName}.${optionalString (description != null) " ${description}"}
      '';
    };

    package = mkOption {
      type = with types; nullOr package;
      default = pkgs.${defaultPackageName};
      defaultText = literalExpression "pkgs.${packageName}";
      example = literalExpression ''
        pkgs.${packageName}.override {
          # See nixpkgs' firefox/wrapper.nix to check which options you can use
          nativeMessagingHosts = [
            # Gnome shell native connector
            pkgs.gnome-browser-connector
            # Tridactyl native connector
            pkgs.tridactyl-native
          ];
        }
      '';
      description = ''
        The ${appName} package to use. If state version ≥ 19.09 then
        this should be a wrapped ${appName} package. For earlier state
        versions it should be an unwrapped ${appName} package.
        Set to `null` to disable installing ${appName}.
      '';
    };

    release = mkOption {
      internal = true;
      type = types.str;
      description = "Upstream release version used to fetch from `releases.mozilla.org`.";
    };

    languagePacks = mkOption {
      type = types.listOf types.str;
      default = [ ];
      description = ''
        The language packs to install. Available language codes can be found
        on the releases page:
        `https://releases.mozilla.org/pub/firefox/releases/''${version}/linux-x86_64/xpi/`,
        replacing `''${version}` with the version of ${appName} you have. If
        the version string of your Firefox derivative diverts from the upstream
        version, try setting the `release` option.
      '';
      example = [
        "en-GB"
        "de"
      ];
    };

    name = mkOption {
      internal = true;
      type = types.str;
      default = name;
      example = "Firefox";
      description = "The name of the browser.";
    };

    wrappedPackageName = mkOption {
      internal = true;
      type = with types; nullOr str;
      default = wrappedPackageName;
      description = "Name of the wrapped browser package.";
    };
    darwinDefaultsId = mkOption rec {
      type = types.nullOr types.str;
      default = if platforms.darwin ? "defaultsId" then platforms.darwin.defaultsId else null;
      example = if default != null then default else "com.developer.app";
      description = "The id for the darwin defaults in order to set policies";
    };

    darwinAppName = mkOption {
      internal = true;
      type = types.str;
      default =
        if platforms.darwin ? "appName" then
          platforms.darwin.appName
        else
          lib.toUpper (lib.substring 0 1 cfg.wrappedPackageName)
          + lib.toLower (
            lib.substring 1 ((lib.stringLength cfg.wrappedPackageName) - 1) cfg.wrappedPackageName
          );
      description = "Name of browser app on Darwin.";
    };

    profilesPath = mkOption {
      internal = true;
      type = types.str;
      default = if isDarwin then "${cfg.configPath}/Profiles" else cfg.configPath;
      description = "Path to profiles.";
    };

    vendorPath = mkOption {
      internal = true;
      type = with types; nullOr str;
      default = null;
      defaultText = literalExpression "platform specific vendor path";
      example = ".mozilla";
      description = "Directory containing the native messaging hosts directory.";
    };

    configPath = mkOption {
      internal = true;
      type = types.str;
      default = with platforms; if isDarwin then darwin.configPath else linux.configPath;
      example = ".mozilla/firefox";
      description = "Directory containing the ${appName} configuration files.";
    };

    nativeMessagingHosts = mkOption {
      type = types.listOf types.package;
      default = [ ];
      description = ''
        Additional packages containing native messaging hosts that should be
        made available to ${appName} extensions.
      '';
    };

    finalPackage = mkOption {
      type = with types; nullOr package;
      readOnly = true;
      description = "Resulting ${appName} package.";
    };

    policies = lib.optionalAttrs (wrappedPackageName != null) (mkOption {
      type = types.attrsOf jsonFormat.type;
      default = { };
      description = "[See list of policies](https://mozilla.github.io/policy-templates/).";
      example = {
        DefaultDownloadDirectory = "\${home}/Downloads";
        BlockAboutConfig = true;
        ExtensionSettings = {
          "uBlock0@raymondhill.net" = {
            install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
            installation_mode = "force_installed";
            default_area = "menupanel";
            private_browsing = true;
          };
        };
      };
    });

    profileVersion = mkOption {
      internal = true;
      type = types.nullOr types.ints.unsigned;
      default = if isDarwin then null else 2;
      description = "profile version, set null for nix-darwin";
    };

    profiles = mkOption {
      type = types.attrsOf (
        types.submodule (
          { config, name, ... }:
          let
            profilePath = modulePath ++ [
              "profiles"
              name
            ];
          in
          {
            imports = [ (pkgs.path + "/nixos/modules/misc/assertions.nix") ];

            options = {
              name = mkOption {
                type = types.str;
                default = name;
                description = "Profile name.";
              };

              id = mkOption {
                type = types.ints.unsigned;
                default = 0;
                description = ''
                  Profile ID. This should be set to a unique number per profile.
                '';
              };

              preConfig = mkOption {
                type = types.lines;
                default = "";
                description = ''
                  Extra preferences to add to {file}`user.js`, before
                  [](#opt-programs.firefox.profiles._name_.settings).

                  Use [](#opt-programs.firefox.profiles._name_.extraConfig), unless
                  you want to overwrite in
                  [](#opt-programs.firefox.profiles._name_.settings), then use this
                  option.
                '';
              };

              settings = mkOption {
                type = types.attrsOf (
                  jsonFormat.type
                  // {
                    description = "${appName} preference (int, bool, string, and also attrs, list, float as a JSON string)";
                  }
                );
                default = { };
                example = literalExpression ''
                  {
                    "browser.startup.homepage" = "https://nixos.org";
                    "browser.search.region" = "GB";
                    "browser.search.isUS" = false;
                    "distribution.searchplugins.defaultLocale" = "en-GB";
                    "general.useragent.locale" = "en-GB";
                    "browser.bookmarks.showMobileBookmarks" = true;
                    "browser.newtabpage.pinned" = [{
                      title = "NixOS";
                      url = "https://nixos.org";
                    }];
                  }
                '';
                description = ''
                  Attribute set of ${appName} preferences.

                  ${appName} only supports int, bool, and string types for
                  preferences, but home-manager will automatically
                  convert all other JSON-compatible values into strings.
                '';
              };

              extraConfig = mkOption {
                type = types.lines;
                default = "";
                description = ''
                  Extra preferences to add to {file}`user.js`.
                '';
              };

              userChrome = mkOption {
                type = types.oneOf [
                  types.lines
                  types.path
                ];
                default = "";
                description = "Custom ${appName} user chrome CSS.";
                example = ''
                  /* Hide tab bar in FF Quantum */
                  @-moz-document url(chrome://browser/content/browser.xul), url(chrome://browser/content/browser.xhtml) {
                    #TabsToolbar {
                      visibility: collapse !important;
                      margin-bottom: 21px !important;
                    }

                    #sidebar-box[sidebarcommand="treestyletab_piro_sakura_ne_jp-sidebar-action"] #sidebar-header {
                      visibility: collapse !important;
                    }
                  }
                '';
              };

              userContent = mkOption {
                type = types.oneOf [
                  types.lines
                  types.path
                ];
                default = "";
                description = "Custom ${appName} user content CSS.";
                example = ''
                  /* Hide scrollbar in FF Quantum */
                  *{scrollbar-width:none !important}
                '';
              };

              bookmarks = mkOption {
                type = (
                  types.coercedTo bookmarkTypes.settingsType
                    (
                      bookmarks:
                      if bookmarks != { } then
                        lib.warn
                          ''
                            ${cfg.name} bookmarks have been refactored into a submodule that now explicitly require a 'force' option to be enabled.

                            Replace:

                            ${moduleName}.profiles.${name}.bookmarks = [ ... ];

                            With:

                            ${moduleName}.profiles.${name}.bookmarks = {
                              force = true;
                              settings = [ ... ];
                            };
                          ''
                          {
                            force = true;
                            settings = bookmarks;
                          }
                      else
                        { }
                    )
                    (
                      types.submodule (
                        { config, ... }:
                        import ./profiles/bookmarks.nix {
                          inherit config lib pkgs;
                          modulePath = modulePath ++ [
                            "profiles"
                            name
                            "bookmarks"
                          ];
                        }
                      )
                    )
                );
                default = { };
                internal = !enableBookmarks;
                description = "Declarative bookmarks.";
              };

              path = mkOption {
                type = types.str;
                default = name;
                description = "Profile path.";
              };

              isDefault = mkOption {
                type = types.bool;
                default = config.id == 0;
                defaultText = "true if profile ID is 0";
                description = "Whether this is a default profile.";
              };

              search = mkOption {
                type = types.submodule (
                  args:
                  import ./profiles/search.nix {
                    inherit (args) config;
                    inherit lib pkgs appName;
                    package = cfg.finalPackage;
                    modulePath = modulePath ++ [
                      "profiles"
                      name
                      "search"
                    ];
                    profilePath = config.path;
                  }
                );
                default = { };
                description = "Declarative search engine configuration.";
              };

              handlers = mkOption {
                type = types.submodule (
                  args:
                  import ./profiles/handlers.nix {
                    inherit (args) config;
                    inherit lib pkgs appName;
                    package = cfg.finalPackage;
                    modulePath = modulePath ++ [
                      "profiles"
                      name
                      "handlers"
                    ];
                    profilePath = config.path;
                  }
                );
                default = { };
                description = "Declarative handlers configuration for MIME types and URL schemes.";
              };

              containersForce = mkOption {
                type = types.bool;
                default = false;
                description = ''
                  Whether to force replace the existing containers configuration.
                  This is recommended since ${appName} will replace the symlink on
                  every launch, but note that you'll lose any existing configuration
                  by enabling this.
                '';
              };

              containers = mkOption {
                type = types.attrsOf (
                  types.submodule (
                    { name, ... }:
                    {
                      options = {
                        name = mkOption {
                          type = types.str;
                          default = name;
                          description = "Container name, e.g., shopping.";
                        };

                        id = mkOption {
                          type = types.ints.unsigned;
                          default = 1;
                          description = ''
                            Container ID. This should be set to a unique number per container in this profile.
                          '';
                        };

                        # List of colors at
                        # https://searchfox.org/mozilla-central/rev/5ad226c7379b0564c76dc3b54b44985356f94c5a/toolkit/components/extensions/parent/ext-contextualIdentities.js#32
                        color = mkOption {
                          type = types.enum [
                            "blue"
                            "turquoise"
                            "green"
                            "yellow"
                            "orange"
                            "red"
                            "pink"
                            "purple"
                            "toolbar"
                          ];
                          default = "pink";
                          description = "Container color.";
                        };

                        icon = mkOption {
                          type = types.enum [
                            "briefcase"
                            "cart"
                            "circle"
                            "dollar"
                            "fence"
                            "fingerprint"
                            "gift"
                            "vacation"
                            "food"
                            "fruit"
                            "pet"
                            "tree"
                            "chill"
                          ];
                          default = "fruit";
                          description = "Container icon.";
                        };
                      };
                    }
                  )
                );
                default = { };
                example = {
                  "shopping" = {
                    id = 1;
                    color = "blue";
                    icon = "cart";
                  };
                  "dangerous" = {
                    id = 2;
                    color = "red";
                    icon = "fruit";
                  };
                };
                description = ''
                  Attribute set of container configurations. See
                  [Multi-Account
                  Containers](https://support.mozilla.org/en-US/kb/containers)
                  for more information.
                '';
              };
              extensions = mkOption {
                type = types.submodule {
                  options = {
                    packages = mkOption {
                      type = types.listOf types.package;
                      default = [ ];
                      example = literalExpression ''
                        with pkgs.nur.repos.rycee.firefox-addons; [
                          privacy-badger
                        ]
                      '';
                      description = ''
                        List of ${name} add-on packages to install for this profile.
                        Some pre-packaged add-ons are accessible from the Nix User Repository.
                        Once you have NUR installed run

                        ```console
                        $ nix-env -f '<nixpkgs>' -qaP -A nur.repos.rycee.firefox-addons
                        ```

                        to list the available ${name} add-ons.

                        Note that it is necessary to manually enable these extensions
                        inside ${name} after the first installation.

                        To automatically enable extensions add
                        `"extensions.autoDisableScopes" = 0;`
                        to
                        [{option}`${moduleName}.profiles.<profile>.settings`](#opt-${moduleName}.profiles._name_.settings)

                        On systems using impermanence, this only prevents
                        ${name} from requiring manual extension approval. It
                        does not preserve extension runtime state such as
                        extension UUIDs, logins, local storage, or
                        per-extension data. Persist the ${name} profile state
                        needed by your extensions, or configure supported
                        extension settings declaratively with
                        [{option}`${moduleName}.profiles.<profile>.extensions.settings`](#opt-${moduleName}.profiles._name_.extensions.settings).

                        Persisting only the `extensions` directory is generally
                        not sufficient, because ${name} stores extension state
                        in other profile files and databases that are managed
                        outside Home Manager.
                      '';
                    };

                    force = mkOption {
                      description = ''
                        Whether to override all previous firefox settings.

                        This is required when using `settings`.
                      '';
                      default = false;
                      example = true;
                      type = types.bool;
                    };

                    exhaustivePermissions = mkOption {
                      description = ''
                        When enabled, the user must authorize requested
                        permissions for all extensions from
                        {option}`${moduleName}.profiles.<profile>.extensions.packages`
                        in
                        {option}`${moduleName}.profiles.<profile>.extensions.settings.<extensionID>.permissions`
                      '';
                      default = false;
                      example = true;
                      type = types.bool;
                    };

                    exactPermissions = mkOption {
                      description = ''
                        When enabled,
                        {option}`${moduleName}.profiles.<profile>.extensions.settings.<extensionID>.permissions`
                        must specify the exact set of permissions that the
                        extension will request.

                        This means that if the authorized permissions are
                        broader than what the extension requests, the
                        assertion will fail.
                      '';
                      default = false;
                      example = true;
                      type = types.bool;
                    };

                    settings = mkOption {
                      default = { };
                      example = literalExpression ''
                        {
                          # Example with uBlock origin's extensionID
                          "uBlock0@raymondhill.net".settings = {
                            selectedFilterLists = [
                              "ublock-filters"
                              "ublock-badware"
                              "ublock-privacy"
                              "ublock-unbreak"
                              "ublock-quick-fixes"
                            ];
                          };

                          # Example with Stylus' UUID-form extensionID
                          "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}".settings = {
                            dbInChromeStorage = true; # required for Stylus
                          }
                        }
                      '';
                      description = ''
                        Attribute set of options for each extension.
                        The keys of the attribute set consist of the ID of the extension
                        or its UUID wrapped in curly braces.
                      '';
                      type = types.attrsOf (
                        types.submodule {
                          options = {
                            settings = mkOption {
                              type = types.attrsOf jsonFormat.type;
                              default = { };
                              description = "Json formatted options for this extension.";
                            };
                            permissions = mkOption {
                              type = types.nullOr (types.listOf types.str);
                              default = null;
                              example = [ "activeTab" ];
                              defaultText = "Any permissions";
                              description = ''
                                Allowed permissions for this extension. See
                                <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>
                                for a list of relevant permissions.
                              '';
                            };
                            force = mkOption {
                              type = types.bool;
                              default = false;
                              example = true;
                              description = ''
                                Forcibly override any existing configuration for
                                this extension.
                              '';
                            };
                          };
                        }
                      );
                    };
                  };
                };
                default = { };
                description = ''
                  Submodule for installing and configuring extensions.
                '';
                example = literalExpression ''
                  {
                    packages = with pkgs.nur.repos.rycee.firefox-addons; [
                      ublock-origin
                    ];
                    settings."uBlock0@raymondhill.net".settings = {
                      selectedFilterLists = [
                        "ublock-filters"
                        "ublock-badware"
                        "ublock-privacy"
                        "ublock-unbreak"
                        "ublock-quick-fixes"
                      ];
                    };
                  }
                '';
              };
            };

            config = {
              assertions = [
                (mkNoDuplicateAssertion config.containers "container")
                {
                  assertion = !(extensionSettingsMissingForce config.extensions.settings) || config.extensions.force;
                  message = ''
                    Using '${lib.showOption profilePath}.extensions.settings' will override all
                    previous extensions settings. Enable either
                    '${lib.showOption profilePath}.extensions.force' or the corresponding
                    '${lib.showOption profilePath}.extensions.settings.<extensionId>.force'
                    to acknowledge this.
                  '';
                }
              ]
              ++ (builtins.concatMap (
                {
                  addonId ? null,
                  name,
                  meta,
                  ...
                }:
                let
                  safeAddonId = if addonId != null then addonId else name;
                  permissions = config.extensions.settings.${safeAddonId}.permissions or null;
                  requireCheck = config.extensions.exhaustivePermissions || permissions != null;
                  authorizedPermissions = lib.optionals (permissions != null) permissions;
                  missingPermissions = lib.subtractLists authorizedPermissions meta.mozPermissions;
                  redundantPermissions = lib.subtractLists meta.mozPermissions authorizedPermissions;
                  checkSatisfied =
                    if config.extensions.exactPermissions then
                      missingPermissions == [ ] && redundantPermissions == [ ]
                    else
                      missingPermissions == [ ];
                  errorMessage =
                    if
                      config.extensions.exactPermissions && missingPermissions != [ ] && redundantPermissions != [ ]
                    then
                      ''
                        Extension ${safeAddonId} requests permissions that weren't
                        authorized: ${builtins.toJSON missingPermissions}.
                        Additionally, the following permissions were authorized,
                        but extension ${safeAddonId} did not request them:
                        ${builtins.toJSON redundantPermissions}.
                        Consider adjusting the permissions in''
                    else if config.extensions.exactPermissions && redundantPermissions != [ ] then
                      ''
                        The following permissions were authorized, but extension
                        ${safeAddonId} did not request them: ${builtins.toJSON redundantPermissions}.
                        Consider removing the redundant permissions from''
                    else
                      ''
                        Extension ${safeAddonId} requests permissions that weren't
                        authorized: ${builtins.toJSON missingPermissions}.
                        Consider adding the missing permissions to'';
                in
                [
                  {
                    assertion = !requireCheck || checkSatisfied;
                    message = ''
                      ${errorMessage}
                      '${
                        lib.showAttrPath (
                          profilePath
                          ++ [
                            "extensions"
                            safeAddonId
                          ]
                        )
                      }.permissions'.
                    '';
                  }
                ]
              ) config.extensions.packages)
              ++ (builtins.concatMap (
                { name, value }:
                let
                  packages = builtins.filter (pkg: (pkg.addonId or pkg.name) == name) config.extensions.packages;
                in
                [
                  {
                    assertion = value.permissions == null || length packages == 1;
                    message = ''
                      Must have exactly one extension with addonId '${name}'
                      in '${lib.showOption profilePath}.extensions.packages' but found ${toString (length packages)}.
                    '';
                  }
                ]
              ) (lib.attrsToList config.extensions.settings))
              ++ config.bookmarks.assertions;
            };
          }
        )
      );
      default = { };
      example = lib.optionalAttrs (moduleName == "programs.firefox") (literalExpression ''
        {
          "dev-edition-default" = {
            id = 0;
            path = config.home.username;

            settings = {
              "browser.aboutConfig.showWarning" = false;
            };
          };
        }
      '');
      description = ''
        Attribute set of ${appName} profiles.

        ${lib.optionalString (moduleName == "programs.firefox") ''
          When using Firefox Developer Edition, the profile name should be
          `dev-edition-default`. You can still set {option}`path` to store the
          profile in a custom directory.
        ''}
      '';
    };

    enableGnomeExtensions = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Whether to enable the GNOME Shell native host connector. Note, you
        also need to set the NixOS option
        `services.gnome.gnome-browser-connector.enable` to
        `true`.
      '';
    };

    pkcs11Modules = mkOption {
      type = types.listOf types.package;
      default = [ ];
      description = ''
        Additional packages to be loaded as PKCS #11 modules in Firefox.
      '';
    };
  };

  config = mkIf cfg.enable (
    {
      assertions = [
        (lib.hm.assertions.assertPlatform moduleName pkgs supportedPlatforms)

        (
          let
            defaults = lib.catAttrs "name" (lib.filter (a: a.isDefault) (attrValues cfg.profiles));
          in
          {
            assertion = cfg.profiles == { } || length defaults == 1;
            message =
              "Must have exactly one default ${appName} profile but found "
              + toString (length defaults)
              + optionalString (length defaults > 1) (", namely " + concatStringsSep ", " defaults);
          }
        )

        (
          let
            getContainers =
              profiles: lib.flatten (mapAttrsToList (_: value: (attrValues value.containers)) profiles);

            findInvalidContainerIds =
              profiles:
              lib.filter (container: container.id == 0 || container.id >= 4294967294) (getContainers profiles);
          in
          {
            assertion = cfg.profiles == { } || length (findInvalidContainerIds cfg.profiles) == 0;
            message = "Container id must be between 1 and 4294967293";
          }
        )

        {
          assertion = cfg.languagePacks == [ ] || cfg.package != null;
          message = ''
            '${moduleName}.languagePacks' requires '${moduleName}.package'
            to be set to a non-null value.
          '';
        }

        (mkNoDuplicateAssertion cfg.profiles "profile")
      ]
      ++ (lib.concatMap (profile: profile.assertions) (attrValues cfg.profiles));

      warnings =
        optional (cfg.enableGnomeExtensions or false) ''
          Using '${moduleName}.enableGnomeExtensions' has been deprecated and
          will be removed in the future. Please change to overriding the package
          configuration using '${moduleName}.package' instead. You can refer to
          its example for how to do this.
        ''
        ++ optional (cfg.vendorPath != null) ''
          Using '${moduleName}.vendorPath' has been deprecated and
          will be removed in the future. Native messaging hosts will function normally without specifying this path.
        '';
      targets.darwin.defaults = (
        mkIf (cfg.darwinDefaultsId != null && isDarwin) {

          ${cfg.darwinDefaultsId} = {
            EnterprisePoliciesEnabled = true;
          }
          // cfg.policies;
        }
      );
      home.packages = lib.optional (cfg.finalPackage != null) cfg.finalPackage;

      home.file = mkMerge (
        [
          {
            "${cfg.configPath}/profiles.ini" = mkIf (cfg.profiles != { }) { text = profilesIni; };
          }
        ]
        ++ lib.flip mapAttrsToList cfg.profiles (
          _: profile:
          # Merge the regular profile settings with extension settings
          mkMerge [
            {
              "${cfg.profilesPath}/${profile.path}/.keep".text = "";

              "${cfg.profilesPath}/${profile.path}/chrome/userChrome.css" = mkIf (profile.userChrome != "") (
                let
                  key = if builtins.isString profile.userChrome then "text" else "source";
                in
                {
                  "${key}" = profile.userChrome;
                }
              );

              "${cfg.profilesPath}/${profile.path}/chrome/userContent.css" = mkIf (profile.userContent != "") (
                let
                  key = if builtins.isString profile.userContent then "text" else "source";
                in
                {
                  "${key}" = profile.userContent;
                }
              );

              "${cfg.profilesPath}/${profile.path}/user.js" =
                mkIf
                  (
                    profile.preConfig != ""
                    || profile.settings != { }
                    || profile.extraConfig != ""
                    || profile.bookmarks.configFile != null
                    || extensionSettingsNeedForce profile.extensions.settings
                  )
                  {
                    text =
                      mkUserJs profile.preConfig profile.settings profile.extraConfig profile.bookmarks.configFile
                        profile.extensions.settings;
                  };

              "${cfg.profilesPath}/${profile.path}/containers.json" = mkIf (profile.containers != { }) {
                text = mkContainersJson profile.containers;
                force = profile.containersForce;
              };

              "${cfg.profilesPath}/${profile.path}/search.json.mozlz4" = mkIf (profile.search.enable) {
                inherit (profile.search) enable force;
                source = profile.search.file;
              };

              "${cfg.profilesPath}/${profile.path}/handlers.json" = mkIf (profile.handlers.enable) {
                source = profile.handlers.configFile;
                inherit (profile.handlers) force;
              };

              "${cfg.profilesPath}/${profile.path}/extensions" = mkIf (profile.extensions.packages != [ ]) {
                source =
                  let
                    extensionsEnvPkg = pkgs.buildEnv {
                      name = "hm-firefox-extensions";
                      paths = profile.extensions.packages;
                    };
                  in
                  "${extensionsEnvPkg}/share/mozilla/${extensionPath}";
                recursive = true;
                force = true;
              };
            }

            (mkMerge (
              mapAttrsToList (
                name: settingConfig:
                mkIf (settingConfig.settings != { }) {
                  "${cfg.profilesPath}/${profile.path}/browser-extension-data/${name}/storage.js" = {
                    force = settingConfig.force || profile.extensions.force;
                    text = lib.generators.toJSON { } settingConfig.settings;
                  };
                }
              ) profile.extensions.settings
            ))
          ]
        )
      );
    }
    // setAttrByPath modulePath {
      finalPackage = wrapPackage cfg.package;
      release = mkOptionDefault (builtins.head (lib.splitString "-" cfg.package.version));

      policies = {
        NoDefaultBookmarks = lib.mkIf (builtins.any (profile: profile.bookmarks.enable) (
          builtins.attrValues cfg.profiles
        )) false;
        ExtensionSettings = lib.mkIf (cfg.languagePacks != [ ]) (
          lib.listToAttrs (
            map (
              lang:
              lib.nameValuePair "langpack-${lang}@firefox.mozilla.org" {
                installation_mode = "normal_installed";
                install_url = "https://releases.mozilla.org/pub/firefox/releases/${cfg.release}/linux-x86_64/xpi/${lang}.xpi";
              }
            ) cfg.languagePacks
          )
        );
      };
    }
  );
}
