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

let
  cfg = config.services.wstunnel;

  argsFormat = {
    type =
      let
        inherit (lib.types)
          attrsOf
          listOf
          oneOf
          bool
          int
          str
          ;
      in
      attrsOf (oneOf [
        bool
        int
        str
        (listOf str)
      ]);
    generate = lib.cli.toCommandLineShellGNU { };
  };

  hostPortToString = { host, port, ... }: "${host}:${toString port}";

  commonOptions = {
    enable = lib.mkEnableOption "this `wstunnel` instance" // {
      default = true;
    };

    package = lib.mkPackageOption pkgs "wstunnel" { };

    autoStart = lib.mkEnableOption "starting this wstunnel instance automatically" // {
      default = true;
    };

    environmentFile = lib.mkOption {
      description = ''
        Environment file to be passed to the systemd service.
        Useful for passing secrets to the service to prevent them from being
        world-readable in the Nix store.
        Note however that the secrets are passed to `wstunnel` through
        the command line, which makes them locally readable for all users of
        the system at runtime.
      '';
      type = lib.types.nullOr lib.types.path;
      default = null;
      example = "/var/lib/secrets/wstunnelSecrets";
    };
  };

  serverSubmodule =
    let
      outerConfig = config;
    in
    { config, ... }:
    let
      certConfig = outerConfig.security.acme.certs.${config.useACMEHost};
    in
    {
      imports = [
        ../../misc/assertions.nix

        (lib.mkRenamedOptionModule
          [
            "enableHTTPS"
          ]
          [
            "listen"
            "enableHTTPS"
          ]
        )
      ]
      ++
        lib.map
          (
            option:
            lib.mkRemovedOptionModule [ option ] ''
              The wstunnel module now uses RFC-42-style settings, please modify your config accordingly
            ''
          )
          [
            "extraArgs"
            "websocketPingInterval"
            "loggingLevel"

            "restrictTo"
            "tlsCertificate"
            "tlsKey"
          ];

      options = commonOptions // {
        listen = lib.mkOption {
          description = ''
            Address and port to listen on.
            Setting the port to a value below 1024 will also give the process
            the required `CAP_NET_BIND_SERVICE` capability.
          '';
          type = lib.types.submodule {
            options = {
              host = lib.mkOption {
                description = "The hostname.";
                type = lib.types.str;
              };
              port = lib.mkOption {
                description = "The port.";
                type = lib.types.port;
              };
              enableHTTPS = lib.mkOption {
                description = "Use HTTPS for the tunnel server.";
                type = lib.types.bool;
                default = true;
              };
            };
          };
          default =
            { config, ... }:
            {
              host = "0.0.0.0";
              port = if config.enableHTTPS then 443 else 80;
            };
          defaultText = lib.literalExpression ''
            { config, ... }:
            {
              host = "0.0.0.0";
              port = if config.enableHTTPS then 443 else 80;
            }
          '';
        };

        useACMEHost = lib.mkOption {
          description = ''
            Use a certificate generated by the NixOS ACME module for the given host.
            Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.
          '';
          type = lib.types.nullOr lib.types.str;
          default = null;
          example = "example.com";
        };

        settings = lib.mkOption {
          type = lib.types.submodule {
            freeformType = argsFormat.type;

            options = {
              restrict-to = lib.mkOption {
                type = lib.types.listOf (
                  lib.types.submodule {
                    options = {
                      host = lib.mkOption {
                        description = "The hostname.";
                        type = lib.types.str;
                      };
                      port = lib.mkOption {
                        description = "The port.";
                        type = lib.types.port;
                      };
                    };
                  }
                );
                default = [ ];
                example = [
                  {
                    host = "127.0.0.1";
                    port = 51820;
                  }
                ];
                description = ''
                  Restrictions on the connections that the server will accept.
                  For more flexibility, and the possibility to also allow reverse tunnels,
                  look into the `restrict-config` option that takes a path to a yaml file.
                '';
              };
            };
          };
          default = { };
          description = ''
            Command line arguments to pass to `wstunnel`.
            Attributes of the form `argName = true;` will be translated to `--argName`,
            and `argName = \"value\"` to `--argName value`.
          '';
          example = {
            "someNewOption" = true;
            "someNewOptionWithValue" = "someValue";
          };
        };
      };

      config = {
        settings = lib.mkIf (config.useACMEHost != null) {
          tls-certificate = "${certConfig.directory}/fullchain.pem";
          tls-private-key = "${certConfig.directory}/key.pem";
        };
      };
    };

  clientSubmodule =
    { config, ... }:
    {
      imports = [
        ../../misc/assertions.nix
      ]
      ++
        lib.map
          (
            option:
            lib.mkRemovedOptionModule [ option ] ''
              The wstunnel module now uses RFC-42-style settings, please modify your config accordingly
            ''
          )
          [
            "extraArgs"
            "websocketPingInterval"
            "loggingLevel"

            "localToRemote"
            "remoteToLocal"
            "httpProxy"
            "soMark"
            "upgradePathPrefix"
            "tlsSNI"
            "tlsVerifyCertificate"
            "upgradeCredentials"
            "customHeaders"
          ];

      options = commonOptions // {
        connectTo = lib.mkOption {
          description = "Server address and port to connect to.";
          type = lib.types.str;
          example = "https://wstunnel.server.com:8443";
        };

        addNetBind = lib.mkEnableOption "Whether add CAP_NET_BIND_SERVICE to the tunnel service, this should be enabled if you want to bind port < 1024";

        settings = lib.mkOption {
          type = lib.types.submodule {
            freeformType = argsFormat.type;

            options = {
              http-headers = lib.mkOption {
                type = lib.types.coercedTo (lib.types.attrsOf lib.types.str) (lib.mapAttrsToList (
                  n: v: "${n}:${v}"
                )) (lib.types.listOf lib.types.str);
                default = { };
                example = {
                  "X-Some-Header" = "some-value";
                };
                description = ''
                  Custom headers to send in the upgrade request
                '';
              };
            };
          };
          default = { };
          description = ''
            Command line arguments to pass to `wstunnel`.
            Attributes of the form `argName = true;` will be translated to `--argName`,
            and `argName = \"value\"` to `--argName value`.
          '';
          example = {
            "someNewOption" = true;
            "someNewOptionWithValue" = "someValue";
          };
        };
      };
    };

  generateServerUnit = name: serverCfg: {
    name = "wstunnel-server-${name}";
    value =
      let
        certConfig = config.security.acme.certs.${serverCfg.useACMEHost};
      in
      {
        description = "wstunnel server - ${name}";
        requires = [
          "network.target"
          "network-online.target"
        ];
        after = [
          "network.target"
          "network-online.target"
        ];
        wantedBy = lib.optional serverCfg.autoStart "multi-user.target";

        serviceConfig = {
          Type = "exec";
          EnvironmentFile = lib.optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
          DynamicUser = true;
          SupplementaryGroups = lib.optional (serverCfg.useACMEHost != null) certConfig.group;
          PrivateTmp = true;
          AmbientCapabilities = lib.optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
          NoNewPrivileges = true;
          RestrictNamespaces = [
            "uts"
            "ipc"
            "pid"
            "user"
            "cgroup"
          ];
          ProtectSystem = "strict";
          ProtectHome = true;
          ProtectKernelTunables = true;
          ProtectKernelModules = true;
          ProtectControlGroups = true;
          PrivateDevices = true;
          RestrictSUIDSGID = true;

          Restart = "on-failure";
          RestartSec = 2;
          RestartSteps = 20;
          RestartMaxDelaySec = "5min";

          ExecStart =
            let
              convertedSettings = serverCfg.settings // {
                restrict-to = lib.map hostPortToString serverCfg.settings.restrict-to;
              };
            in
            ''
              ${lib.getExe serverCfg.package} \
                server \
                ${argsFormat.generate convertedSettings} \
                ${lib.escapeShellArg "${
                  if serverCfg.listen.enableHTTPS then "wss" else "ws"
                }://${hostPortToString serverCfg.listen}"}
            '';
        };
      };
  };

  generateClientUnit = name: clientCfg: {
    name = "wstunnel-client-${name}";
    value = {
      description = "wstunnel client - ${name}";
      requires = [
        "network.target"
        "network-online.target"
      ];
      after = [
        "network.target"
        "network-online.target"
      ];
      wantedBy = lib.optional clientCfg.autoStart "multi-user.target";

      serviceConfig = {
        Type = "exec";
        EnvironmentFile = lib.optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
        DynamicUser = true;
        PrivateTmp = true;
        AmbientCapabilities =
          (lib.optionals clientCfg.addNetBind [ "CAP_NET_BIND_SERVICE" ])
          ++ (lib.optionals ((clientCfg.settings.socket-so-mark or null) != null) [ "CAP_NET_ADMIN" ]);
        NoNewPrivileges = true;
        RestrictNamespaces = [
          "uts"
          "ipc"
          "pid"
          "user"
          "cgroup"
        ];
        ProtectSystem = "strict";
        ProtectHome = true;
        ProtectKernelTunables = true;
        ProtectKernelModules = true;
        ProtectControlGroups = true;
        PrivateDevices = true;
        RestrictSUIDSGID = true;

        Restart = "on-failure";
        RestartSec = 2;
        RestartSteps = 20;
        RestartMaxDelaySec = "5min";

        ExecStart = ''
          ${lib.getExe clientCfg.package} \
            client \
            ${argsFormat.generate clientCfg.settings} \
            ${lib.escapeShellArg clientCfg.connectTo}
        '';
      };
    };
  };
in
{
  options.services.wstunnel = {
    enable = lib.mkEnableOption "wstunnel";

    servers = lib.mkOption {
      description = "`wstunnel` servers to set up.";
      type = lib.types.attrsOf (lib.types.submodule serverSubmodule);
      default = { };
      example = {
        "wg-tunnel" = {
          listen = {
            host = "0.0.0.0";
            port = 8080;
            enableHTTPS = true;
          };
          settings = {
            tls-certificate = "/var/lib/secrets/fullchain.pem";
            tls-private-key = "/var/lib/secrets/key.pem";
            restrict-to = [
              {
                host = "127.0.0.1";
                port = 51820;
              }
            ];
          };
        };
      };
    };

    clients = lib.mkOption {
      description = "`wstunnel` clients to set up.";
      type = lib.types.attrsOf (lib.types.submodule clientSubmodule);
      default = { };
      example = {
        "wg-tunnel" = {
          connectTo = "wss://wstunnel.server.com:8443";
          localToRemote = [
            "tcp://1212:google.com:443"
            "tcp://2:n.lan:4?proxy_protocol"
          ];
          remoteToLocal = [
            "socks5://[::1]:1212"
            "unix://wstunnel.sock:g.com:443"
          ];
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {
    systemd.services =
      (lib.mapAttrs' generateServerUnit (lib.filterAttrs (_: v: v.enable) cfg.servers))
      // (lib.mapAttrs' generateClientUnit (lib.filterAttrs (_: v: v.enable) cfg.clients));

    assertions =
      (lib.mapAttrsToList (name: serverCfg: {
        assertion =
          serverCfg.listen.enableHTTPS
          ->
            (serverCfg.useACMEHost != null)
            || (
              (serverCfg.settings.tls-certificate or null) != null
              && (serverCfg.settings.tls-private-key or null) != null
            );
        message = ''
          If services.wstunnel.servers."${name}".listen.enableHTTPS is set to true, either services.wstunnel.servers."${name}".useACMEHost or both services.wstunnel.servers."${name}".settings.tls-private-key and services.wstunnel.servers."${name}".settings.tls-certificate need to be set.
        '';
      }) cfg.servers)
      ++ (lib.foldlAttrs (
        assertions: _: server:
        assertions ++ server.assertions
      ) [ ] cfg.servers)

      ++ (lib.mapAttrsToList (
        name: clientCfg:
        let
          isListAttrDefined = settings: attr: (settings.${attr} or [ ]) != [ ];
        in
        {
          assertion =
            isListAttrDefined clientCfg.settings "local-to-remote"
            || isListAttrDefined clientCfg.settings "remote-to-local";
          message = ''
            Either one of services.wstunnel.clients."${name}".settings.local-to-remote or services.wstunnel.clients."${name}".settings.remote-to-local must be set.
          '';
        }
      ) cfg.clients)
      ++ (lib.foldlAttrs (
        assertions: _: client:
        assertions ++ client.assertions
      ) [ ] cfg.clients);

    warnings =
      (lib.foldlAttrs (
        warnings: _: server:
        warnings ++ server.warnings
      ) [ ] cfg.servers)
      ++ (lib.foldlAttrs (
        warnings: _: client:
        warnings ++ client.warnings
      ) [ ] cfg.clients);
  };

  meta.maintainers = with lib.maintainers; [
    pentane
    raylas
    rvdp
    neverbehave
  ];
}
