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

let
  cfg = config.services.stargazer;
  globalSection = ''
    listen = ${lib.concatStringsSep " " cfg.listen}
    connection-logging = ${lib.boolToString cfg.connectionLogging}
    log-ip = ${lib.boolToString cfg.ipLog}
    log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
    request-timeout = ${toString cfg.requestTimeout}
    response-timeout = ${toString cfg.responseTimeout}

    [:tls]
    store = ${toString cfg.store}
    organization = ${cfg.certOrg}
    gen-certs = ${lib.boolToString cfg.genCerts}
    regen-certs = ${lib.boolToString cfg.regenCerts}
    ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}

  '';
  genINI = lib.generators.toINI { };
  configFile = pkgs.writeText "config.ini" (
    lib.strings.concatStrings (
      [ globalSection ]
      ++ (lib.lists.forEach cfg.routes (
        section:
        let
          name = section.route;
          params = builtins.removeAttrs section [ "route" ];
        in
        genINI {
          "${name}" = params;
        }
        + "\n"
      ))
    )
  );
in
{
  options.services.stargazer = {
    enable = lib.mkEnableOption "Stargazer Gemini server";

    listen = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
      defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
      example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
      description = ''
        Address and port to listen on.
      '';
    };

    connectionLogging = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Whether or not to log connections to stdout.";
    };

    ipLog = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Log client IP addresses in the connection log.";
    };

    ipLogPartial = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Log partial client IP addresses in the connection log.";
    };

    requestTimeout = lib.mkOption {
      type = lib.types.ints.unsigned;
      default = 5;
      description = ''
        Number of seconds to wait for the client to send a complete
        request. Set to 0 to disable.
      '';
    };

    responseTimeout = lib.mkOption {
      type = lib.types.ints.unsigned;
      default = 0;
      description = ''
        Number of seconds to wait for the client to send a complete
        request and for stargazer to finish sending the response.
        Set to 0 to disable.
      '';
    };

    allowCgiUser = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = ''
        When enabled, the stargazer process will be given `CAP_SETGID`
        and `CAP_SETUID` so that it can run cgi processes as a different
        user. This is required if the `cgi-user` option is used for a route.
        Note that these capabilities could allow privilege escalation so be
        careful. For that reason, this is disabled by default.

        You will need to create the user mentioned `cgi-user` if it does not
        already exist.
      '';
    };

    store = lib.mkOption {
      type = lib.types.path;
      default = /var/lib/gemini/certs;
      description = ''
        Path to the certificate store on disk. This should be a
        persistent directory writable by Stargazer.
      '';
    };

    certOrg = lib.mkOption {
      type = lib.types.str;
      default = "stargazer";
      description = ''
        The name of the organization responsible for the X.509
        certificate's /O name.
      '';
    };

    genCerts = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = ''
        Set to false to disable automatic certificate generation.
        Use if you want to provide your own certs.
      '';
    };

    regenCerts = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = ''
        Set to false to turn off automatic regeneration of expired certificates.
        Use if you want to provide your own certs.
      '';
    };

    certLifetime = lib.mkOption {
      type = lib.types.str;
      default = "";
      description = ''
        How long certs generated by Stargazer should live for.
        Certs live forever by default.
      '';
      example = lib.literalExpression "\"1y\"";
    };

    debugMode = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = "Run Stargazer in debug mode.";
    };

    routes = lib.mkOption {
      type = lib.types.listOf (
        lib.types.submodule {
          freeformType =
            with lib.types;
            attrsOf (
              nullOr (oneOf [
                bool
                int
                float
                str
              ])
              // {
                description = "INI atom (null, bool, int, float or string)";
              }
            );
          options.route = lib.mkOption {
            type = lib.types.str;
            description = "Route section name";
          };
        }
      );
      default = [ ];
      description = ''
        Routes that Stargazer should server.

        Expressed as a list of attribute sets. Each set must have a key `route`
        that becomes the section name for that route in the stargazer ini cofig.
        The remaining keys and values become the parameters for that route.

        [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
      '';
      example = lib.literalExpression ''
        [
          {
            route = "example.com";
            root = "/srv/gemini/example.com"
          }
          {
            route = "example.com:/man";
            root = "/cgi-bin";
            cgi = true;
          }
          {
            route = "other.org~(.*)";
            redirect = "gemini://example.com";
            rewrite = "\1";
          }
        ]
      '';
    };

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

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

  config = lib.mkIf cfg.enable {
    systemd.services.stargazer = {
      description = "Stargazer gemini server";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile} ${lib.optionalString cfg.debugMode "-D"}";
        Restart = "always";
        # User and group
        User = cfg.user;
        Group = cfg.group;
        AmbientCapabilities = lib.mkIf cfg.allowCgiUser [
          "CAP_SETGID"
          "CAP_SETUID"
        ];

        # Hardening
        UMask = "0077";
        PrivateTmp = true;
        ProtectHome = true;
        ProtectSystem = "full";
        ProtectClock = true;
        ProtectHostname = true;
        ProtectControlGroups = true;
        ProtectKernelLogs = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        ProtectProc = "invisible";
        PrivateDevices = true;
        NoNewPrivileges = true;
        RestrictSUIDSGID = true;
        PrivateMounts = true;
        MemoryDenyWriteExecute = true;
        LockPersonality = true;
        RestrictRealtime = true;
        RemoveIPC = true;
        CapabilityBoundingSet = [
          "~CAP_SYS_PTRACE"
          "~CAP_SYS_ADMIN"
          "~CAP_SETPCAP"
          "~CAP_SYS_TIME"
          "~CAP_SYS_PACCT"
          "~CAP_SYS_TTY_CONFIG "
          "~CAP_SYS_CHROOT"
          "~CAP_SYS_BOOT"
          "~CAP_NET_ADMIN"
        ]
        ++ lib.lists.optional (!cfg.allowCgiUser) [
          "~CAP_SETGID"
          "~CAP_SETUID"
        ];
        SystemCallArchitectures = "native";
        SystemCallFilter = [
          "~@cpu-emulation @debug @keyring @mount @obsolete"
        ]
        ++ lib.lists.optional (!cfg.allowCgiUser) [ "@privileged @setuid" ];
      };
    };

    # Create default cert store
    systemd.tmpfiles.rules = lib.mkIf (cfg.store == /var/lib/gemini/certs) [
      ''d /var/lib/gemini/certs - "${cfg.user}" "${cfg.group}" -''
    ];

    users.users = lib.optionalAttrs (cfg.user == "stargazer") {
      stargazer = {
        group = cfg.group;
        isSystemUser = true;
      };
    };

    users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
      stargazer = { };
    };
  };

  meta.maintainers = with lib.maintainers; [ gaykitty ];
}
