{
  config,
  lib,
  pkgs,
  ...
}:
let

  name = "mpd";

  uid = config.ids.uids.mpd;
  gid = config.ids.gids.mpd;
  cfg = config.services.mpd;

  mkKeyValue =
    a:
    lib.mapAttrsToList (
      k: v:
      k
      + " "
      + (
        if builtins.isBool v then
          # Mainly for https://mpd.readthedocs.io/en/stable/user.html#zeroconf
          "\"" + (lib.boolToYesNo v) + "\""
        else
          "\"" + (toString v) + "\""
      )
    ) a;
  settings = lib.filterAttrs (n: v: !(isNull v)) cfg.settings;
  nonBlockSettings = lib.filterAttrs (n: v: !(builtins.isAttrs v || builtins.isList v)) settings;
  pureBlockSettings = removeAttrs settings (builtins.attrNames nonBlockSettings);
  blocks =
    pureBlockSettings
    // lib.optionalAttrs cfg.fluidsynth {
      decoder = (pureBlockSettings.decoder or [ ]) ++ [
        {
          plugin = "fluidsynth";
          soundfont = "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2";
        }
      ];
    };
  processSingleBlock =
    n: v:
    [
      (n + " {")
    ]
    # Add indentation, for better readability
    ++ (map (l: "    " + l) (mkKeyValue v))
    ++ [ "}" ];
  mpdConf = pkgs.writeTextFile {
    name = "mpd.conf";
    text = ''
      # This file was automatically generated by NixOS. Edit mpd's configuration
      # via NixOS' configuration.nix, as this file will be rewritten upon mpd's
      # restart.
    ''
    + lib.concatStringsSep "\n" (
      mkKeyValue (
        {
          state_file = "${cfg.dataDir}/state";
          sticker_file = "${cfg.dataDir}/sticker.sql";
        }
        // nonBlockSettings
      )
      ++ lib.flatten (
        lib.mapAttrsToList (
          n: v: if builtins.isList v then (map (b: processSingleBlock n b) v) else (processSingleBlock n v)
        ) blocks
      )
      ++ lib.imap0 (
        i: a: "password \"{{password-${toString i}}}@${lib.concatStringsSep "," a.permissions}\""
      ) cfg.credentials
    );
    derivationArgs = {
      expectScript = ''
        spawn ${lib.getExe pkgs.buildPackages.mpd} --no-daemon "$env(out)"
        expect {
          "exception: Error in \"$env(out)\"" {
            puts "Config file invalid\n"
            exit 1
          }
          "exception:" {
            exit 0
          }
        }
      '';
      passAsFile = [
        "expectScript"
      ];
    };
    checkPhase = ''
      ${lib.getExe pkgs.buildPackages.expect} -f "$expectScriptPath"
    '';
  };

in
{

  ###### interface

  options = {

    services.mpd = {

      enable = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          Whether to enable MPD, the music player daemon.
        '';
      };

      startWhenNeeded = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          If set, {command}`mpd` is socket-activated; that
          is, instead of having it permanently running as a daemon,
          systemd will start it on the first incoming connection.
        '';
      };

      user = lib.mkOption {
        type = lib.types.str;
        default = name;
        description = "User account under which MPD runs.";
      };

      group = lib.mkOption {
        type = lib.types.str;
        default = name;
        description = "Group account under which MPD runs.";
      };

      dataDir = lib.mkOption {
        type = lib.types.path;
        default = "/var/lib/${name}";
        description = ''
          The directory where MPD stores its state, tag cache, playlists etc. If
          left as the default value this directory will automatically be created
          before the MPD server starts, otherwise the sysadmin is responsible for
          ensuring the directory exists with appropriate ownership and permissions.
        '';
      };

      openFirewall = lib.mkOption {
        type = lib.types.nullOr lib.types.bool;
        default = null;
        description = ''
          Open ports in the firewall for mpd. If `null` (default), you might
          get a warning asking you to set it explicitly to `true` or `false`,
          depending upon the value of `services.mpd.settings.bind_to_address`.
        '';
      };

      settings = lib.mkOption {
        type = lib.types.submodule {
          freeformType =
            let
              inherit (lib.types)
                oneOf
                attrsOf
                listOf
                str
                int
                bool
                path
                ;
              atomType = oneOf [
                str
                int
                bool
                path
              ];
            in
            attrsOf (oneOf [
              atomType
              (listOf (attrsOf atomType))
            ]);
          options = {
            music_directory = lib.mkOption {
              type = with lib.types; either path (strMatching "([a-z]+)://.+");
              default = "${cfg.dataDir}/music";
              defaultText = lib.literalExpression ''"''${dataDir}/music"'';
              description = ''
                The directory or URI where MPD reads music from. If left
                as the default value this directory will automatically be created before
                the MPD server starts, otherwise the sysadmin is responsible for ensuring
                the directory exists with appropriate ownership and permissions.
              '';
            };

            playlist_directory = lib.mkOption {
              type = lib.types.path;
              default = "${cfg.dataDir}/playlists";
              defaultText = lib.literalExpression ''"''${dataDir}/playlists"'';
              description = ''
                The directory where MPD stores playlists. If left as the default value
                this directory will automatically be created before the MPD server starts,
                otherwise the sysadmin is responsible for ensuring the directory exists
                with appropriate ownership and permissions.
              '';
            };

            bind_to_address = lib.mkOption {
              type = lib.types.str;
              default = "127.0.0.1";
              example = "any";
              description = ''
                The address for the daemon to listen on.
                Use `any` to listen on all addresses.
              '';
            };

            port = lib.mkOption {
              type = lib.types.port;
              default = 6600;
              description = ''
                This setting is the TCP port that is desired for the daemon to get assigned
                to.
              '';
            };

            db_file = lib.mkOption {
              type = lib.types.nullOr lib.types.path;
              default = "${cfg.dataDir}/tag_cache";
              defaultText = lib.literalExpression ''"''${dataDir}/tag_cache"'';
              description = ''
                The path to MPD's database.
              '';
            };
          };
        };
        default = { };
        description = ''
          Configuration for MPD. MPD supports key-value like blocks for settings
          like `audio_output` and `neighbor`. Some of these blocks can be
          specified multiple times, so the following configuration:

          ```txt
          audio_output {
              device "iec958:CARD=Intel,DEV=0"
              mixer_control "PCM"
              name "My specific ALSA output"
              type "alsa"
          }
          audio_output {
              mixer_type "null"
              name "ALSA Null"
              type "alsa"
          }
          audio_output {
              name "The Pulse"
              type "pulse"
          }
          ```

          Can be inserted with:

          ```nix
          audio_output = [
            {
              type = "alsa";
              name = "My specific ALSA output";
              device = "iec958:CARD=Intel,DEV=0";
              mixer_control = "PCM";
            }
            {
              type = "alsa";
              name = "ALSA Null";
              mixer_type = "null";
            }
            {
              type = "pulse";
              name = "The Pulse";
            }
          ];
          ```
        '';
      };

      credentials = lib.mkOption {
        type = lib.types.listOf (
          lib.types.submodule {
            options = {
              passwordFile = lib.mkOption {
                type = lib.types.path;
                description = ''
                  Path to file containing the password.
                '';
              };
              permissions =
                let
                  perms = [
                    "read"
                    "add"
                    "player"
                    "control"
                    "admin"
                  ];
                in
                lib.mkOption {
                  type = lib.types.listOf (lib.types.enum perms);
                  default = [ "read" ];
                  description = ''
                    List of permissions that are granted with this password.
                    Permissions can be "${lib.concatStringsSep "\", \"" perms}".
                  '';
                };
            };
          }
        );
        description = ''
          Credentials and permissions for accessing the mpd server.
        '';
        default = [ ];
        example = [
          {
            passwordFile = "/var/lib/secrets/mpd_readonly_password";
            permissions = [ "read" ];
          }
          {
            passwordFile = "/var/lib/secrets/mpd_admin_password";
            permissions = [
              "read"
              "add"
              "player"
              "control"
              "admin"
            ];
          }
        ];
      };

      fluidsynth = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          If set, add fluidsynth soundfont `decoder` block.
        '';
      };
    };

  };

  ###### implementation

  imports = [
    (lib.mkRenamedOptionModule
      [ "services" "mpd" "musicDirectory" ]
      [ "services" "mpd" "settings" "music_directory" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "mpd" "playlistDirectory" ]
      [ "services" "mpd" "settings" "playlist_directory" ]
    )
    (lib.mkRenamedOptionModule [ "services" "mpd" "dbFile" ] [ "services" "mpd" "settings" "db_file" ])
    (lib.mkRenamedOptionModule
      [ "services" "mpd" "network" "listenAddress" ]
      [ "services" "mpd" "settings" "bind_to_address" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "mpd" "network" "port" ]
      [ "services" "mpd" "settings" "port" ]
    )
    (lib.mkRemovedOptionModule
      [
        "services"
        "mpd"
        "extraConfig"
      ]
      "services.mpd.extraConfig was replaced by the declarative services.mpd.settings option, per RFC42."
    )
  ];

  config = lib.mkIf cfg.enable {

    warnings =
      lib.optional
        (
          !(
            (builtins.elem cfg.settings.bind_to_address [
              "localhost"
              "127.0.0.1"
            ])
            || (lib.hasPrefix "/" cfg.settings.bind_to_address)
          )
          && (isNull cfg.openFirewall)
        )
        "Using '${cfg.settings.bind_to_address}' as services.mpd.settings.bind_to_address without enabling services.mpd.openFirewall, might prevent you from accessing MPD from other clients. To suppress this warning, set services.mpd.openFirewall explicitly to `false`";

    # install mpd units
    systemd.packages = [ pkgs.mpd ];

    systemd.sockets.mpd = lib.mkIf cfg.startWhenNeeded {
      wantedBy = [ "sockets.target" ];
      listenStreams = [
        "" # Note: this is needed to override the upstream unit
        (
          if pkgs.lib.hasPrefix "/" cfg.settings.bind_to_address then
            cfg.settings.bind_to_address
          else
            "${
              lib.optionalString (cfg.settings.bind_to_address != "any") "${cfg.settings.bind_to_address}:"
            }${toString cfg.settings.port}"
        )
      ];
    };

    systemd.services.mpd = {
      wantedBy = lib.optional (!cfg.startWhenNeeded) "multi-user.target";

      preStart = ''
        set -euo pipefail
        install -m 600 ${mpdConf} /run/mpd/mpd.conf
      ''
      + lib.optionalString (cfg.credentials != [ ]) (
        lib.concatStringsSep "\n" (
          lib.imap0 (
            i: c:
            "${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf"
          ) cfg.credentials
        )
      );

      serviceConfig = {
        User = "${cfg.user}";
        # Note: the first "" overrides the ExecStart from the upstream unit
        ExecStart = [
          ""
          "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf"
        ];
        RuntimeDirectory = "mpd";
        StateDirectory =
          [ ]
          ++ lib.optionals (cfg.dataDir == "/var/lib/${name}") [ name ]
          ++ lib.optionals (cfg.settings.playlist_directory == "/var/lib/${name}/playlists") [
            name
            "${name}/playlists"
          ]
          ++ lib.optionals (cfg.settings.music_directory == "/var/lib/${name}/music") [
            name
            "${name}/music"
          ];
      };
    };

    networking.firewall.allowedTCPPorts = lib.optionals (
      builtins.isBool cfg.openFirewall && cfg.openFirewall
    ) [ cfg.settings.port ];

    users.users = lib.optionalAttrs (cfg.user == name) {
      ${name} = {
        inherit uid;
        group = cfg.group;
        extraGroups = [ "audio" ];
        description = "Music Player Daemon user";
        home = "${cfg.dataDir}";
      };
    };

    users.groups = lib.optionalAttrs (cfg.group == name) {
      ${name}.gid = gid;
    };
  };

}
