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

with lib;

let
  forEachInstance =
    f:
    flip mapAttrs' config.services.fcgiwrap.instances (
      name: cfg: nameValuePair "fcgiwrap-${name}" (f cfg)
    );

in
{
  imports =
    forEach
      [
        "enable"
        "user"
        "group"
        "socketType"
        "socketAddress"
        "preforkProcesses"
      ]
      (
        attr:
        mkRemovedOptionModule [ "services" "fcgiwrap" attr ] ''
          The global shared fcgiwrap instance is no longer supported due to
          security issues.
          Isolated instances should instead be configured through
          `services.fcgiwrap.instances.*'.
        ''
      );

  options.services.fcgiwrap.instances = mkOption {
    description = "Configuration for fcgiwrap instances.";
    default = { };
    type = types.attrsOf (
      types.submodule (
        { config, ... }:
        {
          options = {
            process.prefork = mkOption {
              type = types.ints.positive;
              default = 1;
              description = "Number of processes to prefork.";
            };

            process.user = mkOption {
              type = types.nullOr types.str;
              default = null;
              description = ''
                User as which this instance of fcgiwrap will be run.
                Set to `null` (the default) to use a dynamically allocated user.
              '';
            };

            process.group = mkOption {
              type = types.nullOr types.str;
              default = null;
              description = "Group as which this instance of fcgiwrap will be run.";
            };

            socket.type = mkOption {
              type = types.enum [
                "unix"
                "tcp"
                "tcp6"
              ];
              default = "unix";
              description = "Socket type: 'unix', 'tcp' or 'tcp6'.";
            };

            socket.address = mkOption {
              type = types.str;
              default = "/run/fcgiwrap-${config._module.args.name}.sock";
              example = "1.2.3.4:5678";
              description = ''
                Socket address.
                In case of a UNIX socket, this should be its filesystem path.
              '';
            };

            socket.user = mkOption {
              type = types.nullOr types.str;
              default = null;
              description = ''
                User to be set as owner of the UNIX socket.
              '';
            };

            socket.group = mkOption {
              type = types.nullOr types.str;
              default = null;
              description = ''
                Group to be set as owner of the UNIX socket.
              '';
            };

            socket.mode = mkOption {
              type = types.nullOr types.str;
              default = if config.socket.type == "unix" then "0600" else null;
              defaultText = literalExpression ''
                if config.socket.type == "unix" then "0600" else null
              '';
              description = ''
                Mode to be set on the UNIX socket.
                Defaults to private to the socket's owner.
              '';
            };
          };
        }
      )
    );
  };

  config = {
    assertions = concatLists (
      mapAttrsToList (name: cfg: [
        {
          assertion = cfg.socket.type == "unix" -> cfg.socket.user != null;
          message = "Socket owner is required for the UNIX socket type.";
        }
        {
          assertion = cfg.socket.type == "unix" -> cfg.socket.group != null;
          message = "Socket owner is required for the UNIX socket type.";
        }
        {
          assertion = cfg.socket.user != null -> cfg.socket.type == "unix";
          message = "Socket owner can only be set for the UNIX socket type.";
        }
        {
          assertion = cfg.socket.group != null -> cfg.socket.type == "unix";
          message = "Socket owner can only be set for the UNIX socket type.";
        }
        {
          assertion = cfg.socket.mode != null -> cfg.socket.type == "unix";
          message = "Socket mode can only be set for the UNIX socket type.";
        }
      ]) config.services.fcgiwrap.instances
    );

    systemd.services = forEachInstance (cfg: {
      after = [ "nss-user-lookup.target" ];
      wantedBy = optional (cfg.socket.type != "unix") "multi-user.target";

      serviceConfig = {
        ExecStart = ''
          ${pkgs.fcgiwrap}/sbin/fcgiwrap ${
            cli.toCommandLineShellGNU { } (
              {
                c = cfg.process.prefork;
              }
              // (optionalAttrs (cfg.socket.type != "unix") {
                s = "${cfg.socket.type}:${cfg.socket.address}";
              })
            )
          }
        '';
      }
      // (
        if cfg.process.user != null then
          {
            User = cfg.process.user;
            Group = cfg.process.group;
          }
        else
          {
            DynamicUser = true;
          }
      );
    });

    systemd.sockets = forEachInstance (
      cfg:
      mkIf (cfg.socket.type == "unix") {
        wantedBy = [ "sockets.target" ];
        socketConfig = {
          ListenStream = cfg.socket.address;
          SocketUser = cfg.socket.user;
          SocketGroup = cfg.socket.group;
          SocketMode = cfg.socket.mode;
        };
      }
    );
  };
}
