{ config, lib, ... }:

let
  cfg = config.services.proxy;
  canonicalize = builtins.replaceStrings [ "*" "." ":" "[" "]" ] [ "all" "_" "_" "" "" ];
in
{

  options.services.proxy = {
    enable = lib.mkOption {
      default = false;
      description = "whether to enable proxy";
      type = lib.types.bool;
    };

    proxyHosts = lib.mkOption {
      type = lib.types.listOf (lib.types.submodule {
        options = {
          hostNames = lib.mkOption {
            type = with lib.types; listOf str;
            default = [ ];
            description = ''
              Proxy these hostNames.
            '';
          };

          proxyTo = lib.mkOption {
            type = lib.types.submodule {
              options = {
                host = lib.mkOption {
                  type = with lib.types; nullOr str;
                  default = null;
                  description = ''
                    Host to forward traffic to.
                    Any hostname may only be used once
                  '';
                };
                httpPort = lib.mkOption {
                  type = lib.types.port;
                  default = 80;
                  description = ''
                    Port to forward http to.
                  '';
                };
                httpsPort = lib.mkOption {
                  type = lib.types.port;
                  default = 443;
                  description = ''
                    Port to forward http to.
                  '';
                };
                proxyHttpPort = lib.mkOption {
                  type = lib.types.port;
                  default = 8080;
                  description = ''
                    Port to forward http to when using the proxy protocol.
                  '';
                };
                proxyHttpsPort = lib.mkOption {
                  type = lib.types.port;
                  default = 8443;
                  description = ''
                    Port to forward http to when using the proxy protocol.
                  '';
                };
              };
            };
            description = ''
              { host = /* ip or fqdn */; httpPort = 80; httpsPort = 443; } to proxy to
            '';
            default = { };
          };

          proxyProtocol = lib.mkOption {
            type = lib.types.bool;
            default = true;
            description = "Whether to use proxy protocol to connect to the server.";
          };

          matchArg = lib.mkOption {
            type = lib.types.str;
            default = "";
            description = "Optional argument to HAProxy `req.ssl_sni -i`";
          };
        };
      });
      default = [ ];
      example = [{
        hostNames = [ "test.hq.c3d2.de" "test.c3d2.de" ];
        proxyTo = {
          host = "172.22.99.99";
          httpPort = 80;
          httpsPort = 443;
        };
      }];
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [{
      assertion = let
        proxyTo = lib.forEach cfg.proxyHosts (x: x.proxyTo.host);
        uniqueProxyTo = lib.unique proxyTo;
      in
        (builtins.length proxyTo) == (builtins.length uniqueProxyTo);
      message = "proxyTo must be unique across the list of proxyHosts. Insert your domain you want to proxy to the list of hostNames instead.";
    }];

    services.haproxy = {
      enable = true;
      config = ''
        # TODO: upstream to nixos-modules
        global
          ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
          ssl-dh-param-file ${config.security.dhparams.params.nginx.path}

        defaults
          timeout client 30000
          timeout connect 5000
          timeout check 5000
          timeout server 30000

        frontend http-in
          # tfo is tcp fastopen
          bind :::80 tfo v4v6
          option http-keep-alive
          default_backend proxy-backend-http

        backend proxy-backend-http
          mode http
          option http-server-close
          option forwardfor
          http-request set-header X-Forwarded-Proto http
          http-request set-header X-Forwarded-Port 80
          ${lib.concatMapStrings ({ proxyTo, proxyProtocol, hostNames, matchArg }:
              lib.optionalString (hostNames != [ ] && proxyTo.host != null) (
                lib.concatMapStrings (hostname: ''

                  use-server ${canonicalize hostname}-http if { req.hdr(host) -i ${matchArg} ${hostname} }
                    server ${canonicalize hostname}-http ${proxyTo.host}:${toString proxyTo.httpPort} check ${lib.optionalString proxyProtocol "backup"}
                   ${lib.optionalString proxyProtocol "server ${canonicalize hostname}-proxy-http ${proxyTo.host}:${toString proxyTo.proxyHttpPort} check send-proxy-v2"}
                '') hostNames
              )
            ) cfg.proxyHosts
          }

        frontend https-in
          # tfo is tcp fastopen
          bind :::443 tfo v4v6
          tcp-request inspect-delay 5s
          tcp-request content accept if { req.ssl_hello_type 1 }

          ${lib.concatMapStrings ({ proxyTo, hostNames, matchArg, ... }:
              lib.concatMapStrings (hostname: ''
                use_backend ${canonicalize proxyTo.host}-https if { req.ssl_sni -i ${matchArg} ${hostname} }
              '') hostNames
          ) cfg.proxyHosts}

        ${lib.concatMapStrings ({ proxyTo, proxyProtocol, ...  }: ''

          backend ${canonicalize proxyTo.host}-https
            server ${canonicalize proxyTo.host}-https ${proxyTo.host}:${toString proxyTo.httpsPort} check ${lib.optionalString proxyProtocol "backup"}
            ${lib.optionalString proxyProtocol "server ${canonicalize proxyTo.host}-proxy-https ${proxyTo.host}:${toString proxyTo.proxyHttpsPort} check send-proxy-v2"}
        '') cfg.proxyHosts}
      '';
    };
  };
}
