{
  config,
  lib,
  pkgs,
  modulesPath,
  ...
}:
let
  cfg = config.khscodes.services.nginx;
  locationOptions = import "${modulesPath}/services/web-servers/nginx/location-options.nix" {
    inherit lib config;
  };
  logfmt = ''
    log_format logfmt escape=default
      'ts="$time_iso8601" http_host="$http_host" request_method="$request_method" '
      'uri="$request_uri" ' # full path
      'status=$status ' # response status
      'remote_addr="$remote_addr" remote_port="$remote_port" ' # remote ip and port (port should be dynamic)
      'http_user_agent="$http_user_agent" '
      'http_referer="$http_referer" '
      'http_x_forwarded_for="$http_x_forwarded_for" '
      'server_name="$server_name" ' # the name of the vhost serving the request, may be different from http_host
      'scheme="$scheme" ' # http/https
      'body_bytes_sent=$body_bytes_sent ' # the number of body bytes exclude headers sent to a client
      'bytes_sent=$bytes_sent ' # the number of bytes sent to a client
      'ssl_protocol="$ssl_protocol" ' # TLS protocol
      'ssl_cipher="$ssl_cipher" ' # TLS cipher
      'server_protocol="$server_protocol" ' # request protocol, like HTTP/1.1 or HTTP/2.0
      'remote_user="$remote_user" ' # http user name (should be unused for this setup)
      'pid="$pid" ' # process pid
      'connection="$connection" ' # connection serial number
      'connection_requests="$connection_requests" ' # number of requests made in connection
      'request_id="$request_id" ' # unique id for the request
      'request_length=$request_length ' # request length (including headers and body)
      'gzip_ratio="$gzip_ratio" '
      'request_time=$request_time ' # request processing time in seconds with msec resolutio
      'upstream="$upstream_addr" ' # upstream backend server for proxied requests
      'upstream_connect_time=$upstream_connect_time ' # upstream handshake time incl. TLS
      'upstream_header_time=$upstream_header_time ' # time spent receiving upstream headers
      'upstream_response_time=$upstream_response_time ' # time spent receiving upstream body
      'upstream_response_length=$upstream_response_length ' # upstream response length
      'upstream_cache_status="$upstream_cache_status" ' # cache HIT/MISS where applicable
      'msec=$msec' # unix timestamp with millis in fraction
      ;
  '';
  vhostOption = lib.khscodes.mkSubmodule {
    description = "nginx vhost";
    options = {
      acme = lib.mkOption {
        description = "If a simple certificate for the virtual host name itself is not desired auto configured, then set this option. If set to a string it will be used as `useAcmeHost` from NixOS nginx service configuration. Otherwise set to the acme submodule and configure the desired certificate that way";
        type = lib.types.nullOr (
          lib.types.oneOf [
            lib.types.str
            (lib.khscodes.mkSubmodule {
              description = "acme certificate";
              options = {
                domains = lib.mkOption {
                  type = lib.types.listOf lib.types.str;
                  description = "Domain names the certificate should be requested for, should include the virtual host itself";
                };
              };
            })
          ]
        );
        default = null;
      };
      globalRedirect = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        description = "If set, all requests for this host are redirected (defaults to 301, configurable with redirectCode) to the given hostname.";
      };
      redirectCode = lib.mkOption {
        type = lib.types.int;
        default = 301;
        description = "HTTP status used by globalRedirect and forceSSL. Possible usecases include temporary (302, 307) redirects, keeping the request method and body (307, 308), or explicitly resetting the method to GET (303). See https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections.";
      };
      mtls = lib.mkOption {
        type = lib.types.nullOr (
          lib.khscodes.mkSubmodule {
            options = {
              verify = lib.mkOption {
                type = lib.types.enum [
                  "optional"
                  "on"
                ];
                default = "on";
              };
              certificate = lib.mkOption {
                type = lib.types.str;
                description = "Path to the certificate to verify client certificates against";
              };
            };
            description = "Nginx MTLS settings";
          }
        );
        default = null;
      };
      robotsTxt = lib.mkOption {
        type = lib.types.path;
        description = "Path to robots.txt file, by default everything is disallowed";
        default = ./robots.txt;
      };
      extraConfig = lib.mkOption {
        type = lib.types.lines;
        description = "Extra configuration to inject into the generated nginx config";
        default = '''';
      };
      rateLimit = {
        enable = lib.mkOption {
          type = lib.types.bool;
          default = true;
          description = "Enable rate limiting";
        };
        burst = lib.mkOption {
          type = lib.types.int;
          default = 20;
        };
      };
      fail2ban.enable = lib.mkOption {
        type = lib.types.bool;
        default = true;
        description = "Enable fail2ban rate limiting";
      };
      locations = lib.mkOption {
        type = lib.types.attrsOf (
          lib.khscodes.mkSubmodule {
            description = "nginx virtual host location";
            options = locationOptions.options;
          }
        );
        default = { };
      };
    };
  };
  dns01Enabled = config.khscodes.security.acme.dns01Enabled;
  useAcmeConfiguration = lib.attrsets.foldlAttrs (
    acc: name: item:
    acc || (item.acme != null && !lib.attrsets.isAttrs item.acme)
  ) false cfg.virtualHosts;
  modernSslAppendedHttpConfig =
    if cfg.sslConfiguration == "modern" then
      ''
        ssl_ecdh_curve X25519:prime256v1:secp384r1;
      ''
    else
      '''';
in
{
  options.khscodes.services.nginx = {
    enable = lib.mkEnableOption "Enables nginx";
    sslConfiguration = lib.mkOption {
      type = lib.types.enum [
        "modern"
        "intermediate"
      ];
      description = ''
        Which sort of ssl configuration following https://ssl-config.mozilla.org/#server=nginx&version=1.28.0&config=modern&openssl=3.4.1&guideline=5.7 as a baseline to generate.
        The generated config is not guarenteed to follow this template specifically. In general, modern is preferred, intermediate should only be used if there's a specific reason to do so.
        Do note that intermediate requires generating dhparams of large size, which can take hours to complete.

        TODO: Look into OCSP stapling.
      '';
      default = "modern";
    };
    virtualHosts = lib.mkOption {
      type = lib.types.attrsOf vhostOption;
      description = "Virtual hosts settings";
      default = { };
    };
  };
  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = !useAcmeConfiguration || dns01Enabled;
        message = "Cannot use `config.khscodes.services.nginx.virtualHosts.<name>.acme = {}` without setting config.khscodes.security.acme.dns01Enabled";
      }
    ];
    services.prometheus.exporters = {
      nginx = {
        enable = config.khscodes.infrastructure.vault-prometheus-sender.enable;
      };
      nginxlog = {
        enable = config.khscodes.infrastructure.vault-prometheus-sender.enable;
        settings = {
          namespaces = lib.lists.map (vhost: {
            name = vhost;
            format = "$remote_addr - $remote_user [$time_local] \"$request\" $status $body_bytes_sent \"$http_referer\" \"$http_user_agent\" \"$http_x_forwarded_for\" $upstream_response_time";
            metrics_override = {
              prefix = "";
            };
            namespace_label = "vhost";
            source = {
              files = [ "/var/log/nginx/access.${vhost}.log" ];
            };
            histogram_buckets = [
              0.005
              0.01
              0.025
              0.05
              0.1
              0.25
              0.5
              1
              2.5
              5
              10
            ];
          }) (lib.attrsets.attrNames cfg.virtualHosts);
        };
      };
    };
    systemd.services.prometheus-nginxlog-exporter.serviceConfig.DynamicUser = false;
    khscodes.infrastructure.vault-prometheus-sender.exporters.enabled = [
      "nginx"
      "nginxlog"
    ];
    users = {
      users.${config.services.prometheus.exporters.nginxlog.user} = {
        group = config.services.prometheus.exporters.nginxlog.group;
        extraGroups = [ config.services.nginx.group ];
        isSystemUser = true;
      };
      groups.${config.services.prometheus.exporters.nginxlog.user} = { };
    };
    systemd.services.nginx = {
      serviceConfig = {
        ExecStartPre = [
          ''${pkgs.uutils-coreutils-noprefix}/bin/touch /var/log/nginx/access.fail2ban.log''
          ''${pkgs.uutils-coreutils-noprefix}/bin/chmod 064 /var/log/nginx/access.fail2ban.log''
        ];
      };
    };
    systemd.services.fail2ban = {
      # fail2ban fails starting if the nginx log files don't exist
      after = [ "nginx.service" ];
    };
    services.fail2ban.jails = {
      nginx-botsearch = {
        settings = {
          filter = "nginx-botsearch";
          port = "http,https";
          logpath = "/var/log/nginx/access.fail2ban.log";
          backend = "auto";
          findtime = 600;
          maxretry = 5;
        };
      };
      nginx-bad-request = {
        settings = {
          filter = "nginx-bad-request";
          port = "http,https";
          logpath = "/var/log/nginx/access.fail2ban.log";
          backend = "auto";
          findtime = 600;
          maxretry = 30;
        };
      };
      nginx-req-limit = {
        settings = {
          filter = "nginx-limit-req";
          port = "http,https";
          backend = "systemd";
          findtime = 600;
          maxretry = 5;
        };
      };
    };
    khscodes.networking.aliases = lib.attrsets.attrNames cfg.virtualHosts;
    khscodes.security.acme.enable = true;
    security.dhparams.enable = lib.mkIf (cfg.sslConfiguration == "intermediate") {
      enable = true;
      params."nginx" = {
        bits = 4096;
      };
    };
    services.nginx = {
      enable = true;
      package = lib.mkDefault (
        pkgs.nginxStable.overrideAttrs (oldAttrs: {
          nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.pkg-config ];
        })
      );
      statusPage = config.khscodes.infrastructure.vault-prometheus-sender.enable;
      sslDhparam = lib.mkIf (
        cfg.sslConfiguration == "intermediate"
      ) "${config.security.dhparams.params."nginx".path}"; # DHParams only used when using the ciphers of intermediate
      sslProtocols = lib.mkIf (cfg.sslConfiguration == "modern") "TLSv1.3"; # The default matches intermediate
      sslCiphers = lib.mkIf (cfg.sslConfiguration == "modern") null;
      recommendedTlsSettings = lib.mkDefault true;
      recommendedGzipSettings = lib.mkDefault true;
      recommendedOptimisation = lib.mkDefault true;
      recommendedZstdSettings = lib.mkDefault true;
      recommendedProxySettings = lib.mkDefault true;
      commonHttpConfig = ''
        ${logfmt}
        access_log /var/log/nginx/access.logfmt.log logfmt;
        log_format nginx_exporter '$remote_addr - $remote_user [$time_local] "$request" '
                                  '$status $body_bytes_sent "$http_referer" '
                                  '"$http_user_agent" "$http_x_forwarded_for" $upstream_response_time';
      '';
      appendHttpConfig = ''
        limit_req_zone $binary_remote_addr zone=nobots:10m rate=20r/s;
        map $scheme $hsts_header {
          https "max-age=63072000; preload";
        }
        add_header Strict-Transport-Security $hsts_header;

        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;

        map $http_referer $httpReferer {
          default "$http_referer";
          "" "(direct)";
        }

        map $http_user_agent $httpAgent {
          default "$http_user_agent";
          "" "Unknown";
        }

        ${modernSslAppendedHttpConfig}
      '';
      virtualHosts = lib.attrsets.mapAttrs (
        name: value:
        let
          mtls =
            if value.mtls != null then
              ''
                ssl_client_certificate ${value.mtls.certificate};
                ssl_verify_client ${value.mtls.verify};
              ''
            else
              '''';
          reqLimit = lib.strings.optionalString value.rateLimit.enable ''
            limit_req zone=nobots burst=${toString value.rateLimit.burst} nodelay;
          '';
          extraConfig = ''
            ${mtls}
            ${reqLimit}
            access_log /var/log/nginx/access.logfmt.log logfmt;
            access_log /var/log/nginx/access.${name}.log nginx_exporter;
            ${value.extraConfig}
          '';
        in
        {
          inherit (value)
            globalRedirect
            redirectCode
            ;
          inherit extraConfig;
          locations = {
            "=/robots.txt" = {
              alias = value.robotsTxt;
            };
          } // value.locations;
          forceSSL = true;
          enableACME = value.acme == null && !dns01Enabled;
          useACMEHost =
            if lib.strings.isString value.acme then
              value.acme
            else if lib.attrsets.isAttrs value.acme || dns01Enabled then
              name
            else
              null;
        }
      ) cfg.virtualHosts;
    };
    networking.firewall.allowedTCPPorts = [
      80
      443
    ];
    networking.firewall.allowedUDPPorts = [ 443 ];
    users.users.nginx.extraGroups = lib.lists.optional dns01Enabled "acme";
    khscodes.infrastructure.vault-loki-sender = {
      extraFiles = [ ./nginx.alloy ];
      extraGroups = [ "nginx" ];
    };
    security.acme.certs = lib.mkIf dns01Enabled (
      lib.attrsets.foldlAttrs (
        acc: name: value:
        (
          acc
          // (lib.attrsets.optionalAttrs
            (lib.attrsets.isAttrs value.acme || (dns01Enabled && !lib.strings.isString value.acme))
            {
              "${name}" =
                if value.acme == null then
                  {
                    domain = name;
                    reloadServices = [ "nginx" ];
                  }
                else
                  {
                    domain = lib.lists.head value.acme.domains;
                    extraDomainNames = lib.lists.tail value.acme.domains;
                    reloadServices = [ "nginx" ];
                  };
            }
          )
        )
      ) { } cfg.virtualHosts
    );
  };
}
