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

with lib;

let
  cfg = config.services.sftpgo;
  defaultUser = "sftpgo";
  settingsFormat = pkgs.formats.json {};
  configFile = settingsFormat.generate "sftpgo.json" cfg.settings;
  hasPrivilegedPorts = any (port: port > 0 && port < 1024) (
    catAttrs "port" (cfg.settings.httpd.bindings
      ++ cfg.settings.ftpd.bindings
      ++ cfg.settings.sftpd.bindings
      ++ cfg.settings.webdavd.bindings
    )
  );
in
{
  options.services.sftpgo = {
    enable = mkOption {
      type = types.bool;
      default = false;
      description = "sftpgo";
    };

    package = mkPackageOption pkgs "sftpgo" { };

    extraArgs = mkOption {
      type = with types; listOf str;
      default = [];
      description = ''
        Additional command line arguments to pass to the sftpgo daemon.
      '';
      example = [ "--log-level" "info" ];
    };

    dataDir = mkOption {
      type = types.str;
      default = "/var/lib/sftpgo";
      description = ''
        The directory where SFTPGo stores its data files.
      '';
    };

    user = mkOption {
      type = types.str;
      default = defaultUser;
      description = ''
        User account name under which SFTPGo runs.
      '';
    };

    group = mkOption {
      type = types.str;
      default = defaultUser;
      description = ''
        Group name under which SFTPGo runs.
      '';
    };

    loadDataFile = mkOption {
      default = null;
      type = with types; nullOr path;
      description = ''
        Path to a json file containing users and folders to load (or update) on startup.
        Check the [documentation](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md)
        for the `--loaddata-from` command line argument for more info.
      '';
    };

    settings = mkOption {
      default = {};
      description = ''
        The primary sftpgo configuration. See the
        [configuration reference](https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md)
        for possible values.
      '';
      type = with types; submodule {
        freeformType = settingsFormat.type;
        options = {
          httpd.bindings = mkOption {
            default = [];
            description = ''
              Configure listen addresses and ports for httpd.
            '';
            type = types.listOf (types.submodule {
              freeformType = settingsFormat.type;
              options = {
                address = mkOption {
                  type = types.str;
                  default = "127.0.0.1";
                  description = ''
                    Network listen address. Leave blank to listen on all available network interfaces.
                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
                  '';
                };

                port = mkOption {
                  type = types.port;
                  default = 8080;
                  description = ''
                    The port for serving HTTP(S) requests.

                    Setting the port to `0` disables listening on this interface binding.
                  '';
                };

                enable_web_admin = mkOption {
                  type = types.bool;
                  default = true;
                  description = ''
                    Enable the built-in web admin for this interface binding.
                  '';
                };

                enable_web_client = mkOption {
                  type = types.bool;
                  default = true;
                  description = ''
                    Enable the built-in web client for this interface binding.
                  '';
                };
              };
            });
          };

          ftpd.bindings = mkOption {
            default = [];
            description = ''
              Configure listen addresses and ports for ftpd.
            '';
            type = types.listOf (types.submodule {
              freeformType = settingsFormat.type;
              options = {
                address = mkOption {
                  type = types.str;
                  default = "127.0.0.1";
                  description = ''
                    Network listen address. Leave blank to listen on all available network interfaces.
                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
                  '';
                };

                port = mkOption {
                  type = types.port;
                  default = 0;
                  description = ''
                    The port for serving FTP requests.

                    Setting the port to `0` disables listening on this interface binding.
                  '';
                };
              };
            });
          };

          sftpd.bindings = mkOption {
            default = [];
            description = ''
              Configure listen addresses and ports for sftpd.
            '';
            type = types.listOf (types.submodule {
              freeformType = settingsFormat.type;
              options = {
                address = mkOption {
                  type = types.str;
                  default = "127.0.0.1";
                  description = ''
                    Network listen address. Leave blank to listen on all available network interfaces.
                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
                  '';
                };

                port = mkOption {
                  type = types.port;
                  default = 0;
                  description = ''
                    The port for serving SFTP requests.

                    Setting the port to `0` disables listening on this interface binding.
                  '';
                };
              };
            });
          };

          webdavd.bindings = mkOption {
            default = [];
            description = ''
              Configure listen addresses and ports for webdavd.
            '';
            type = types.listOf (types.submodule {
              freeformType = settingsFormat.type;
              options = {
                address = mkOption {
                  type = types.str;
                  default = "127.0.0.1";
                  description = ''
                    Network listen address. Leave blank to listen on all available network interfaces.
                    On *NIX you can specify an absolute path to listen on a Unix-domain socket.
                  '';
                };

                port = mkOption {
                  type = types.port;
                  default = 0;
                  description = ''
                    The port for serving WebDAV requests.

                    Setting the port to `0` disables listening on this interface binding.
                  '';
                };
              };
            });
          };

          smtp = mkOption {
            default = {};
            description = ''
              SMTP configuration section.
            '';
            type = types.submodule {
              freeformType = settingsFormat.type;
              options = {
                host = mkOption {
                  type = types.str;
                  default = "";
                  description = ''
                    Location of SMTP email server. Leave empty to disable email sending capabilities.
                  '';
                };

                port = mkOption {
                  type = types.port;
                  default = 465;
                  description = "Port of the SMTP Server.";
                };

                encryption = mkOption {
                  type = types.enum [ 0 1 2 ];
                  default = 1;
                  description = ''
                    Encryption scheme:
                    - `0`: No encryption
                    - `1`: TLS
                    - `2`: STARTTLS
                  '';
                };

                auth_type = mkOption {
                  type = types.enum [ 0 1 2 ];
                  default = 0;
                  description = ''
                    - `0`: Plain
                    - `1`: Login
                    - `2`: CRAM-MD5
                  '';
                };

                user = mkOption {
                  type = types.str;
                  default = "sftpgo";
                  description = "SMTP username.";
                };

                from = mkOption {
                  type = types.str;
                  default = "SFTPGo <sftpgo@example.com>";
                  description = ''
                    From address.
                  '';
                };
              };
            };
          };
        };
      };
    };
  };

  config = mkIf cfg.enable {
    services.sftpgo.settings = (mapAttrs (name: mkDefault) {
      ftpd.bindings = [{ port = 0; }];
      httpd.bindings = [{ port = 0; }];
      sftpd.bindings = [{ port = 0; }];
      webdavd.bindings = [{ port = 0; }];
      httpd.openapi_path = "${cfg.package}/share/sftpgo/openapi";
      httpd.templates_path = "${cfg.package}/share/sftpgo/templates";
      httpd.static_files_path = "${cfg.package}/share/sftpgo/static";
      smtp.templates_path = "${cfg.package}/share/sftpgo/templates";
    });

    users = optionalAttrs (cfg.user == defaultUser) {
      users = {
        ${defaultUser} = {
          description = "SFTPGo system user";
          isSystemUser = true;
          group = defaultUser;
          home = cfg.dataDir;
        };
      };

      groups = {
        ${defaultUser} = {
          members = [ defaultUser ];
        };
      };
    };

    systemd.services.sftpgo = {
      description = "SFTPGo daemon";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];

      environment = {
        SFTPGO_CONFIG_FILE = mkDefault configFile;
        SFTPGO_LOG_FILE_PATH = mkDefault ""; # log to journal
        SFTPGO_LOADDATA_FROM = mkIf (cfg.loadDataFile != null) cfg.loadDataFile;
      };

      serviceConfig = mkMerge [
        ({
          Type = "simple";
          User = cfg.user;
          Group = cfg.group;
          WorkingDirectory = cfg.dataDir;
          ReadWritePaths = [ cfg.dataDir ];
          LimitNOFILE = 8192; # taken from upstream
          KillMode = "mixed";
          ExecStart = "${cfg.package}/bin/sftpgo serve ${utils.escapeSystemdExecArgs cfg.extraArgs}";
          ExecReload = "${pkgs.util-linux}/bin/kill -s HUP $MAINPID";

          # Service hardening
          CapabilityBoundingSet = [ (optionalString hasPrivilegedPorts "CAP_NET_BIND_SERVICE") ];
          DevicePolicy = "closed";
          LockPersonality = true;
          NoNewPrivileges = true;
          PrivateDevices = true;
          PrivateTmp = true;
          ProcSubset = "pid";
          ProtectClock = true;
          ProtectControlGroups = true;
          ProtectHome = true;
          ProtectHostname = true;
          ProtectKernelLogs = true;
          ProtectKernelModules = true;
          ProtectKernelTunables = true;
          ProtectProc = "invisible";
          ProtectSystem = "strict";
          RemoveIPC = true;
          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
          RestrictNamespaces = true;
          RestrictRealtime = true;
          RestrictSUIDSGID = true;
          SystemCallArchitectures = "native";
          SystemCallFilter = [ "@system-service" "~@privileged" ];
          UMask = "0077";
        })
        (mkIf hasPrivilegedPorts {
          AmbientCapabilities = "CAP_NET_BIND_SERVICE";
        })
        (mkIf (cfg.dataDir == options.services.sftpgo.dataDir.default) {
          StateDirectory = baseNameOf cfg.dataDir;
        })
      ];
    };
  };
}
