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

with lib;

let
  cfg = config.services.webhook;
  defaultUser = "webhook";

  hookFormat = pkgs.formats.json { };

  hookType = types.submodule (
    { name, ... }:
    {
      freeformType = hookFormat.type;
      options = {
        id = mkOption {
          type = types.str;
          default = name;
          description = ''
            The ID of your hook. This value is used to create the HTTP endpoint (`protocol://yourserver:port/prefix/''${id}`).
          '';
        };
        execute-command = mkOption {
          type = types.str;
          description = "The command that should be executed when the hook is triggered.";
        };
      };
    }
  );

  hookFiles =
    mapAttrsToList (name: hook: hookFormat.generate "webhook-${name}.json" [ hook ]) cfg.hooks
    ++ mapAttrsToList (
      name: hook: pkgs.writeText "webhook-${name}.json.tmpl" "[${hook}]"
    ) cfg.hooksTemplated;

in
{
  options = {
    services.webhook = {
      enable = mkEnableOption ''
        [Webhook](https://github.com/adnanh/webhook), a server written in Go that allows you to create HTTP endpoints (hooks),
        which execute configured commands for any person or service that knows the URL
      '';

      package = mkPackageOption pkgs "webhook" { };
      user = mkOption {
        type = types.str;
        default = defaultUser;
        description = ''
          Webhook will be run under this user.

          If set, you must create this user yourself!
        '';
      };
      group = mkOption {
        type = types.str;
        default = defaultUser;
        description = ''
          Webhook will be run under this group.

          If set, you must create this group yourself!
        '';
      };
      ip = mkOption {
        type = types.str;
        default = "0.0.0.0";
        description = ''
          The IP webhook should serve hooks on.

          The default means it can be reached on any interface if `openFirewall = true`.
        '';
      };
      port = mkOption {
        type = types.port;
        default = 9000;
        description = "The port webhook should be reachable from.";
      };
      openFirewall = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Open the configured port in the firewall for external ingress traffic.
          Preferably the Webhook server is instead put behind a reverse proxy.
        '';
      };
      enableTemplates = mkOption {
        type = types.bool;
        default = cfg.hooksTemplated != { };
        defaultText = literalExpression "hooksTemplated != {}";
        description = ''
          Enable the generated hooks file to be parsed as a Go template.
          See [the documentation](https://github.com/adnanh/webhook/blob/master/docs/Templates.md) for more information.
        '';
      };
      urlPrefix = mkOption {
        type = types.str;
        default = "hooks";
        description = ''
          The URL path prefix to use for served hooks (`protocol://yourserver:port/''${prefix}/hook-id`).
        '';
      };
      hooks = mkOption {
        type = types.attrsOf hookType;
        default = { };
        example = {
          echo = {
            execute-command = "echo";
            response-message = "Webhook is reachable!";
          };
          redeploy-webhook = {
            execute-command = "/var/scripts/redeploy.sh";
            command-working-directory = "/var/webhook";
          };
        };
        description = ''
          The actual configuration of which hooks will be served.

          Read more on the [project homepage] and on the [hook definition] page.
          At least one hook needs to be configured.

          [hook definition]: https://github.com/adnanh/webhook/blob/master/docs/Hook-Definition.md
          [project homepage]: https://github.com/adnanh/webhook#configuration
        '';
      };
      hooksTemplated = mkOption {
        type = types.attrsOf types.str;
        default = { };
        example = {
          echo-template = ''
            {
              "id": "echo-template",
              "execute-command": "echo",
              "response-message": "{{ getenv "MESSAGE" }}"
            }
          '';
        };
        description = ''
          Same as {option}`hooks`, but these hooks are specified as literal strings instead of Nix values,
          and hence can include [template syntax](https://github.com/adnanh/webhook/blob/master/docs/Templates.md)
          which might not be representable as JSON.

          Template syntax requires the {option}`enableTemplates` option to be set to `true`, which is
          done by default if this option is set.
        '';
      };
      verbose = mkOption {
        type = types.bool;
        default = true;
        description = "Whether to show verbose output.";
      };
      extraArgs = mkOption {
        type = types.listOf types.str;
        default = [ ];
        example = [ "-secure" ];
        description = ''
          These are arguments passed to the webhook command in the systemd service.
          You can find the available arguments and options in the [documentation][parameters].

          [parameters]: https://github.com/adnanh/webhook/blob/master/docs/Webhook-Parameters.md
        '';
      };
      environment = mkOption {
        type = types.attrsOf types.str;
        default = { };
        description = "Extra environment variables passed to webhook.";
      };
    };
  };

  config = mkIf cfg.enable {
    assertions =
      let
        overlappingHooks = builtins.intersectAttrs cfg.hooks cfg.hooksTemplated;
      in
      [
        {
          assertion = hookFiles != [ ];
          message = "At least one hook needs to be configured for webhook to run.";
        }
        {
          assertion = overlappingHooks == { };
          message = "`services.webhook.hooks` and `services.webhook.hooksTemplated` have overlapping attribute(s): ${concatStringsSep ", " (builtins.attrNames overlappingHooks)}";
        }
      ];

    users.users = mkIf (cfg.user == defaultUser) {
      ${defaultUser} = {
        isSystemUser = true;
        group = cfg.group;
        description = "Webhook daemon user";
      };
    };

    users.groups = mkIf (cfg.user == defaultUser && cfg.group == defaultUser) {
      ${defaultUser} = { };
    };

    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];

    systemd.services.webhook = {
      description = "Webhook service";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      environment = config.networking.proxy.envVars // cfg.environment;
      script =
        let
          args = [
            "-ip"
            cfg.ip
            "-port"
            (toString cfg.port)
            "-urlprefix"
            cfg.urlPrefix
          ]
          ++ concatMap (hook: [
            "-hooks"
            hook
          ]) hookFiles
          ++ optional cfg.enableTemplates "-template"
          ++ optional cfg.verbose "-verbose"
          ++ cfg.extraArgs;
        in
        ''
          ${cfg.package}/bin/webhook ${escapeShellArgs args}
        '';
      serviceConfig = {
        Restart = "on-failure";
        User = cfg.user;
        Group = cfg.group;
      };
    };
  };
}
