{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    literalExpression
    mkOption
    optional
    types
    ;

  cfg = config.services.dunst;

  eitherStrBoolIntList = with types; either str (either bool (either int (listOf str)));

  toDunstIni = lib.generators.toINI {
    mkKeyValue =
      key: value:
      let
        value' =
          if lib.isBool value then
            (lib.hm.booleans.yesNo value)
          else if lib.isString value then
            ''"${value}"''
          else
            toString value;
      in
      "${key}=${value'}";
  };

  themeType = types.submodule {
    options = {
      package = mkOption {
        type = types.package;
        example = literalExpression "pkgs.adwaita-icon-theme";
        description = "Package providing the theme.";
      };

      name = mkOption {
        type = types.str;
        example = "Adwaita";
        description = "The name of the theme within the package.";
      };

      size = mkOption {
        type = types.str;
        default = "32x32";
        example = "16x16";
        description = "The desired icon size.";
      };
    };
  };

  hicolorTheme = {
    package = pkgs.hicolor-icon-theme;
    name = "hicolor";
    size = "32x32";
  };

in
{
  meta.maintainers = [ lib.maintainers.rycee ];

  options = {
    services.dunst = {
      enable = lib.mkEnableOption "the dunst notification daemon";

      package = mkOption {
        type = types.package;
        default = pkgs.dunst;
        defaultText = literalExpression "pkgs.dunst";
        description = "Package providing {command}`dunst`.";
      };

      configFile = mkOption {
        type = with types; nullOr (either str path);
        default = null;
        description = ''
          Path to the configuration file read by dunst.

          Note that the configuration generated by Home Manager will be
          written to {file}`$XDG_CONFIG_HOME/dunst/dunstrc`
          regardless. This allows using a mutable configuration file generated
          from the immutable one, useful in scenarios where live reloading is
          desired.
        '';
      };

      iconTheme = mkOption {
        type = themeType;
        default = hicolorTheme;
        description = "Set the icon theme.";
      };

      waylandDisplay = mkOption {
        type = types.str;
        default = "";
        description = "Set the service's {env}`WAYLAND_DISPLAY` environment variable.";
      };

      settings = mkOption {
        type = types.submodule {
          freeformType = with types; attrsOf (attrsOf eitherStrBoolIntList);
          options = {
            global.icon_path = mkOption {
              type = types.separatedString ":";
              description = "Paths where dunst will look for icons.";
            };
          };
        };
        default = { };
        description = "Configuration written to {file}`$XDG_CONFIG_HOME/dunst/dunstrc`.";
        example = literalExpression ''
          {
            global = {
              width = 300;
              height = 300;
              offset = "30x50";
              origin = "top-right";
              transparency = 10;
              frame_color = "#eceff1";
              font = "Droid Sans 9";
            };

            urgency_normal = {
              background = "#37474f";
              foreground = "#eceff1";
              timeout = 10;
            };
          };
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable (
    lib.mkMerge [
      {
        assertions = [
          (lib.hm.assertions.assertPlatform "services.dunst" pkgs lib.platforms.linux)
        ];

        home.packages = [ cfg.package ];

        xdg.dataFile."dbus-1/services/org.knopwob.dunst.service".source =
          "${pkgs.dunst}/share/dbus-1/services/org.knopwob.dunst.service";

        services.dunst.settings.global.icon_path =
          let
            useCustomTheme =
              cfg.iconTheme.package != hicolorTheme.package
              || cfg.iconTheme.name != hicolorTheme.name
              || cfg.iconTheme.size != hicolorTheme.size;

            basePaths = [
              "/run/current-system/sw"
              config.home.profileDirectory
              cfg.iconTheme.package
            ] ++ optional useCustomTheme hicolorTheme.package;

            themes = [
              cfg.iconTheme
            ] ++ optional useCustomTheme (hicolorTheme // { size = cfg.iconTheme.size; });

            categories = [
              "actions"
              "animations"
              "apps"
              "categories"
              "devices"
              "emblems"
              "emotes"
              "filesystem"
              "intl"
              "legacy"
              "mimetypes"
              "places"
              "status"
              "stock"
            ];

            mkPath =
              {
                basePath,
                theme,
                category,
              }:
              "${basePath}/share/icons/${theme.name}/${theme.size}/${category}";
          in
          lib.concatMapStringsSep ":" mkPath (
            lib.cartesianProduct {
              basePath = basePaths;
              theme = themes;
              category = categories;
            }
          );

        systemd.user.services.dunst = {
          Unit = {
            Description = "Dunst notification daemon";
            After = [ config.wayland.systemd.target ];
            PartOf = [ config.wayland.systemd.target ];
          };

          Service = {
            Type = "dbus";
            BusName = "org.freedesktop.Notifications";
            ExecStart = lib.escapeShellArgs (
              [ "${cfg.package}/bin/dunst" ]
              ++
                # Using `-config` breaks dunst's drop-ins, so only use it when an alternative path is set
                lib.optionals (cfg.configFile != null) [
                  "-config"
                  cfg.configFile
                ]
            );
            Environment = lib.optionalString (cfg.waylandDisplay != "") "WAYLAND_DISPLAY=${cfg.waylandDisplay}";
          };
        };
      }

      (lib.mkIf (cfg.settings != { }) {
        xdg.configFile."dunst/dunstrc" = {
          text = toDunstIni cfg.settings;
          onChange = ''
            ${pkgs.procps}/bin/pkill -u "$USER" ''${VERBOSE+-e} dunst || true
          '';
        };
      })
    ]
  );
}
