{
  pkgs,
  lib,
  config,
  ...
}:
let
  cfg = config.security.crowdsec;
  settingsFormat = pkgs.formats.yaml { };

  hub = pkgs.fetchFromGitHub {
    owner = "crowdsecurity";
    repo = "hub";
    rev = "7a3b4753f4577257c0cbeb8f8f90c7f17d2ae008";
    hash = "sha256-HB4jHyhiO8gjBkLmpo6bDbwhfm5m5nAtNlKhDkZjt2I=";
  };

  cscli = pkgs.writeShellScriptBin "cscli" ''
    export PATH="$PATH:${cfg.package}/bin/"

    sudo=exec
    if [ "$USER" != "crowdsec" ]; then
        sudo='exec /run/wrappers/bin/sudo -u crowdsec'
    fi

    $sudo ${cfg.package}/bin/cscli "$@"
  '';

  acquisitions = ''
    ---
    ${lib.concatMapStringsSep "\n---\n" builtins.toJSON cfg.acquisitions}
    ---
  '';
in
{
  imports = [ ./remediations ];

  options.security.crowdsec =
    let
      inherit (lib.types)
        nullOr
        listOf
        package
        path
        str
        ;
    in
    {
      enable = lib.mkEnableOption "crowdsec";

      package = lib.mkOption {
        type = package;
        default = pkgs.crowdsec;
      };

      stateDirectory = lib.mkOption {
        type = path;
        readOnly = true;

        description = ''
          The state directory of the crowdsec instance. Cannot be
          changed, but is exposed for downstream use.
        '';
      };

      settings = lib.mkOption {
        inherit (settingsFormat) type;
        default = { };

        description = ''
          The crowdsec configuration. Refer to
          <https://docs.crowdsec.net/docs/next/configuration/crowdsec_configuration/>
          for details on supported values.
        '';
      };

      parserWhitelist = lib.mkOption {
        type = listOf str;
        default = [ ];
        description = ''
          Set of IP addresses to add to a parser-based whitelist.

          Addresses can be specified either as plain IP addresses or
          in CIDR notation.
        '';
      };

      acquisitions = lib.mkOption {
        type = listOf settingsFormat.type;
        default = [ ];
        description = ''
          Log acquisitions.
        '';
      };

      extraGroups = lib.mkOption {
        type = listOf str;
        default = [ ];
        description = ''
          Additional groups to make the service part of.

          Required to permit reading from various log sources.
        '';
      };

      hubConfigurations = {
        collections = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec collections to install.
          '';
        };

        scenarios = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec scenarios to install.
          '';
        };

        parsers = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec parsers to install.
          '';
        };

        postoverflows = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec postoverflows to install.
          '';
        };

        appsecConfigs = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec appsec configurations to install.
          '';
        };

        appsecRules = lib.mkOption {
          type = listOf str;
          description = ''
            List of pre-made crowdsec appsec rules to install.
          '';
        };
      };

      centralApiCredentials = lib.mkOption {
        type = nullOr path;
        default = null;

        description = ''
          The API key to access crowdsec's central API - this is
          required to access any of the shared blocklists.

          Use of this feature is optional, entering no API key (the
          default) turns all sharing or receiving of blocked IPs off.

          Note that adding the API key by itself does not enable
          sharing of blocked IPs with the central API. This limits the
          types of blocklists this instance can access.

          To also turn sharing blocked IPs on, set
          `api.server.online_client.sharing = true;`.
        '';
      };

      ctiApiKey = lib.mkOption {
        type = nullOr path;
        default = null;

        description = ''
          The API key for crowdsec's CTI offering.
        '';
      };
    };

  config = lib.mkIf cfg.enable {
    # Set up default settings; anything that *shouldn't* be changed is
    # set to the default priority so that users need to use
    # `lib.mkForce`.
    security.crowdsec = {
      stateDirectory = "/var/lib/crowdsec";

      settings = {
        common = {
          daemonize = true;
          # The default logs to files, which isn't the preferred way
          # on NixOS
          log_media = "stdout";
        };

        config_paths = {
          config_dir = "${cfg.stateDirectory}/config/";
          data_dir = "${cfg.stateDirectory}/data/";
          # This "config" file is intended to be written to using the
          # cscli tool, so you can temporarily make it so rules don't
          # do anything but log what they *would* do for
          # experimentation.
          simulation_path = "${cfg.stateDirectory}/config/simulation.yaml";

          pattern_dir = lib.mkDefault "${cfg.package}/share/crowdsec/config/patterns";

          hub_dir = hub;
          index_path = "${hub}/.index.json";

          # Integrations aren't supported for now
          notification_dir = lib.mkDefault "/var/empty/";
          plugin_dir = lib.mkDefault "/var/empty/";
        };

        crowdsec_service.acquisition_path =
          # Using an if/else here because `mkMerge` does not work in
          # YAML-type options
          if cfg.acquisitions == [ ] then
            "${cfg.package}/share/crowdsec/config/acquis.yaml"
          else
            pkgs.writeText "acquis.yaml" acquisitions;

        cscli = {
          prometheus_uri = lib.mkDefault "127.0.0.1:6060";
        };

        db_config = {
          type = lib.mkDefault "sqlite";
          db_path = lib.mkDefault "${cfg.stateDirectory}/data/crowdsec.db";
          use_wal = lib.mkDefault true;
          flush = {
            max_items = lib.mkDefault 5000;
            max_age = lib.mkDefault "7d";
          };
        };

        api = {
          cti = {
            enabled = cfg.ctiApiKey != null;
            key = cfg.ctiApiKey;
          };
          client.credentials_path = "${cfg.stateDirectory}/local_credentials.yaml";
          server = {
            listen_uri = lib.mkDefault "127.0.0.1:8080";
            profiles_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/profiles.yaml";
            console_path = lib.mkDefault "${cfg.package}/share/crowdsec/config/console.yaml";

            online_client = {
              # By default, we don't let crowdsec phone home, since
              # this is usually within NixOS users' concerns.
              sharing = lib.mkDefault false;
              credentials_path = cfg.centralApiCredentials;
            };
          };
        };

        # We enable prometheus by default, since cscli relies on it
        # for metrics
        prometheus = {
          enabled = lib.mkDefault true;
          level = lib.mkDefault "full";
          listen_addr = lib.mkDefault "127.0.0.1";
          listen_port = lib.mkDefault 6060;
        };
      };
    };

    systemd.packages = [ cfg.package ];

    environment = {
      systemPackages = [
        # To add completions; sadly need to hand-roll this since
        # neither `symlinkJoin` nor `buildEnv` have collision
        # handling.
        (pkgs.runCommandNoCCLocal "cscli" { } ''
          mkdir -p $out
          ln -s ${cscli}/bin $out/bin
          ln -s ${cfg.package}/share $out/share
        '')
      ];

      etc."crowdsec/config.yaml".source = settingsFormat.generate "crowdsec-settings.yaml" cfg.settings;
    };

    systemd = {
      tmpfiles.settings."10-crowdsec" = {
        "${cfg.stateDirectory}".d = {
          user = "crowdsec";
          group = "crowdsec";
          mode = "0700";
        };

        # This must be created for the setup service to work
        "${cfg.stateDirectory}/config".d = {
          user = "crowdsec";
          group = "crowdsec";
          mode = "0700";
        };

        "${cfg.stateDirectory}/config/parsers".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
          user = "crowdsec";
          group = "crowdsec";
          mode = "0700";
        };

        "${cfg.stateDirectory}/config/parsers/s02-enrich".d = lib.mkIf (cfg.parserWhitelist != [ ]) {
          user = "crowdsec";
          group = "crowdsec";
          mode = "0700";
        };

        "${cfg.stateDirectory}/config/parsers/s02-enrich/nixos-whitelist.yaml" =
          lib.mkIf (cfg.parserWhitelist != [ ])
            {
              "L+".argument =
                (settingsFormat.generate "crowdsec-nixos-whitelist.yaml" {
                  name = "nixos/parser-whitelist";
                  description = "Parser whitelist generated by the crowdsec NixOS module";
                  whitelist = {
                    reason = "Filtered by NixOS whitelist";
                    ip = lib.lists.filter (ip: !(lib.hasInfix "/" ip)) cfg.parserWhitelist;
                    cidr = lib.lists.filter (ip: lib.hasInfix "/" ip) cfg.parserWhitelist;
                  };
                }).outPath;
            };
      };

      services = {
        crowdsec-setup = {
          # TODO(tlater): Depend on tmpfiles path for
          # /var/lib/crowdsec/config
          description = "Crowdsec database and config preparation";

          script = ''
            if [ ! -e '${cfg.settings.config_paths.simulation_path}' ]; then
                cp '${cfg.package}/share/crowdsec/config/simulation.yaml' '${cfg.settings.config_paths.simulation_path}'
            fi

            if [ ! -e '${cfg.settings.api.client.credentials_path}' ]; then
                ${cfg.package}/bin/cscli machines add --auto --file '${cfg.settings.api.client.credentials_path}'
            fi
          '';

          serviceConfig = {
            User = "crowdsec";
            Group = "crowdsec";
            StateDirectory = "crowdsec";

            Type = "oneshot";
            RemainAfterExit = true;
          };
        };

        # Note that the service basics are already defined upstream
        crowdsec = {
          enable = true;

          after = [ "crowdsec-setup.service" ];
          bindsTo = [ "crowdsec-setup.service" ];
          wantedBy = [ "multi-user.target" ];

          serviceConfig = {
            User = "crowdsec";
            Group = "crowdsec";
            SupplementaryGroups = cfg.extraGroups;

            StateDirectory = "crowdsec";
          };
        };
      };
    };

    users = {
      users.crowdsec = {
        isSystemUser = true;
        home = cfg.stateDirectory;
        group = "crowdsec";
      };
      groups = {
        crowdsec = { };
      };
    };
  };
}
