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

let
  cfg = config.shb.certs;

  inherit (builtins) dirOf;
  inherit (lib)
    flatten
    mapAttrsToList
    optionalAttrs
    optionals
    unique
    ;
in
{
  imports = [
    ../../lib/module.nix
    ./monitoring.nix
  ];

  options.shb.certs = {
    systemdService = lib.mkOption {
      description = ''
        Systemd oneshot service used to generate the Certificate Authority bundle.
      '';
      type = lib.types.str;
      default = "shb-ca-bundle.service";
    };
    enableDashboard = lib.mkEnableOption "the SSL SHB dashboard" // {
      default = true;
    };
    cas.selfsigned = lib.mkOption {
      description = "Generate a self-signed Certificate Authority.";
      default = { };
      type = lib.types.attrsOf (
        lib.types.submodule (
          { config, ... }:
          {
            options = {
              name = lib.mkOption {
                type = lib.types.str;
                description = ''
                  Certificate Authority Name. You can put what you want here, it will be displayed by the
                  browser.
                '';
                default = "Self Host Blocks Certificate";
              };

              paths = lib.mkOption {
                description = ''
                  Paths where CA certs will be located.

                  This option implements the SSL Generator contract.
                '';
                type = shb.contracts.ssl.certs-paths;
                default = {
                  key = "/var/lib/certs/cas/${config._module.args.name}.key";
                  cert = "/var/lib/certs/cas/${config._module.args.name}.cert";
                };
              };

              systemdService = lib.mkOption {
                description = ''
                  Systemd oneshot service used to generate the certs.

                  This option implements the SSL Generator contract.
                '';
                type = lib.types.str;
                default = "shb-certs-ca-${config._module.args.name}.service";
              };
            };
          }
        )
      );
    };
    certs.selfsigned = lib.mkOption {
      description = "Generate self-signed certificates signed by a Certificate Authority.";
      default = { };
      type = lib.types.attrsOf (
        lib.types.submodule (
          { config, ... }:
          {
            options = {
              ca = lib.mkOption {
                type = lib.types.nullOr shb.contracts.ssl.cas;
                description = ''
                  CA used to generate this certificate. Only used for self-signed.

                  This contract input takes the contract output of the `shb.certs.cas` SSL block.
                '';
                default = null;
              };

              domain = lib.mkOption {
                type = lib.types.str;
                description = ''
                  Domain to generate a certificate for. This can be a wildcard domain like
                  `*.example.com`.
                '';
                example = "example.com";
              };

              extraDomains = lib.mkOption {
                type = lib.types.listOf lib.types.str;
                description = ''
                  Other domains to generate a certificate for.
                '';
                default = [ ];
                example = lib.literalExpression ''
                  [
                    "sub1.example.com"
                    "sub2.example.com"
                  ]
                '';
              };

              group = lib.mkOption {
                type = lib.types.str;
                description = ''
                  Unix group owning this certificate.
                '';
                default = "root";
                example = "nginx";
              };

              paths = lib.mkOption {
                description = ''
                  Paths where certs will be located.

                  This option implements the SSL Generator contract.
                '';
                type = shb.contracts.ssl.certs-paths;
                default = {
                  key = "/var/lib/certs/selfsigned/${config._module.args.name}.key";
                  cert = "/var/lib/certs/selfsigned/${config._module.args.name}.cert";
                };
              };

              systemdService = lib.mkOption {
                description = ''
                  Systemd oneshot service used to generate the certs.

                  This option implements the SSL Generator contract.
                '';
                type = lib.types.str;
                default = "shb-certs-cert-selfsigned-${config._module.args.name}.service";
              };

              reloadServices = lib.mkOption {
                description = ''
                  The list of systemd services to call `systemctl try-reload-or-restart` on.
                '';
                type = lib.types.listOf lib.types.str;
                default = [ ];
                example = [ "nginx.service" ];
              };
            };
          }
        )
      );
    };

    certs.letsencrypt = lib.mkOption {
      description = "Generate certificates signed by [Let's Encrypt](https://letsencrypt.org/).";
      default = { };
      type = lib.types.attrsOf (
        lib.types.submodule (
          { config, ... }:
          {
            options = {
              domain = lib.mkOption {
                type = lib.types.str;
                description = ''
                  Domain to generate a certificate for. This can be a wildcard domain like
                  `*.example.com`.
                '';
                example = "example.com";
              };

              extraDomains = lib.mkOption {
                type = lib.types.listOf lib.types.str;
                description = ''
                  Other domains to generate a certificate for.
                '';
                default = [ ];
                example = lib.literalExpression ''
                  [
                    "sub1.example.com"
                    "sub2.example.com"
                  ]
                '';
              };

              paths = lib.mkOption {
                description = ''
                  Paths where certs will be located.

                  This option implements the SSL Generator contract.
                '';
                type = shb.contracts.ssl.certs-paths;
                default = {
                  key = "/var/lib/acme/${config._module.args.name}/key.pem";
                  cert = "/var/lib/acme/${config._module.args.name}/cert.pem";
                };
              };

              group = lib.mkOption {
                type = lib.types.nullOr lib.types.str;
                description = ''
                  Unix group owning this certificate.
                '';
                default = "acme";
                example = "nginx";
              };

              systemdService = lib.mkOption {
                description = ''
                  Systemd oneshot service used to generate the certs.

                  This option implements the SSL Generator contract.
                '';
                type = lib.types.str;
                default = "shb-certs-cert-letsencrypt-${config._module.args.name}.service";
              };

              afterAndWants = lib.mkOption {
                description = ''
                  Systemd service(s) that must start successfully before attempting to reach acme.
                '';
                type = lib.types.listOf lib.types.str;
                default = [ ];
                example = lib.literalExpression ''
                  [ "dnsmasq.service" ]
                '';
              };

              reloadServices = lib.mkOption {
                description = ''
                  The list of systemd services to call `systemctl try-reload-or-restart` on.
                '';
                type = lib.types.listOf lib.types.str;
                default = [ ];
                example = [ "nginx.service" ];
              };

              dnsProvider = lib.mkOption {
                description = ''
                  DNS provider to use.

                  See https://go-acme.github.io/lego/dns/ for the list of supported providers.

                  If null is given, use instead the reverse proxy to validate the domain.
                '';
                type = lib.types.nullOr lib.types.str;
                default = null;
                example = "linode";
              };

              dnsResolver = lib.mkOption {
                description = "IP of a DNS server used to resolve hostnames.";
                type = lib.types.str;
                default = "8.8.8.8";
              };

              credentialsFile = lib.mkOption {
                type = lib.types.nullOr lib.types.path;
                description = ''
                  Credentials file location for the chosen DNS provider.

                  The content of this file must expose environment variables as written in the
                  [documentation](https://go-acme.github.io/lego/dns/) of each DNS provider.

                  For example, if the documentation says the credential must be located in the environment
                  variable DNSPROVIDER_TOKEN, then the file content must be:

                  DNSPROVIDER_TOKEN=xyz

                  You can put non-secret environment variables here too or use shb.ssl.additionalcfg instead.
                '';
                example = "/run/secrets/ssl";
                default = null;
              };

              additionalEnvironment = lib.mkOption {
                type = lib.types.attrsOf lib.types.str;
                default = { };
                description = ''
                  Additional environment variables used to configure the DNS provider.

                  For secrets, use shb.ssl.credentialsFile instead.

                  See the chosen provider's [documentation](https://go-acme.github.io/lego/dns/) for
                  available options.
                '';
                example = lib.literalExpression ''
                  {
                    DNSPROVIDER_TIMEOUT = "10";
                    DNSPROVIDER_PROPAGATION_TIMEOUT = "240";
                  }
                '';
              };

              makeAvailableToUser = lib.mkOption {
                type = lib.types.nullOr lib.types.str;
                description = ''
                  Make all certificates available to given user.
                '';
                default = null;
              };

              adminEmail = lib.mkOption {
                description = "Admin email in case certificate retrieval goes wrong.";
                type = lib.types.str;
              };

              stagingServer = lib.mkOption {
                description = "User Let's Encrypt's staging server.";
                type = lib.types.bool;
                default = false;
              };

              debug = lib.mkOption {
                description = "Enable debug logging";
                type = lib.types.bool;
                default = false;
              };
            };
          }
        )
      );
    };
  };

  config =
    let
      serviceName = lib.strings.removeSuffix ".service";
    in
    lib.mkMerge [
      # Generic assertions
      {
        assertions = lib.flatten (
          lib.mapAttrsToList (
            _name: certCfg:
            (
              let
                domainInExtraDomains =
                  (lib.lists.findFirstIndex (x: x == certCfg.domain) null certCfg.extraDomains) != null;
                firstDuplicateDomain =
                  (
                    l:
                    let
                      sorted = lib.sort (x: y: x < y) l;
                      maybeDupe = lib.lists.removePrefix (lib.lists.commonPrefix (lib.uniqueStrings sorted) sorted) sorted;
                    in
                    if maybeDupe == [ ] then null else lib.head maybeDupe
                  )
                    certCfg.extraDomains;
              in
              [
                {
                  assertion = !domainInExtraDomains;
                  message = "Error in SHB option for domain ${certCfg.domain}: do not repeat the domain name in the `extraDomain` option.";
                }
                {
                  assertion = firstDuplicateDomain == null;
                  message = "Error in SHB option for domain ${certCfg.domain}: `extraDomain` option cannot have duplicates, first offender is: ${firstDuplicateDomain} in the following list: ${lib.concatStringsSep " " certCfg.extraDomains}.";
                }
              ]
            )
          ) (cfg.certs.selfsigned // cfg.certs.letsencrypt)
        );
      }
      # Config for self-signed CA.
      {
        systemd.services = lib.mapAttrs' (
          _name: caCfg:
          lib.nameValuePair (serviceName caCfg.systemdService) {
            wantedBy = [ "multi-user.target" ];
            wants = [ config.shb.certs.systemdService ];
            before = [ config.shb.certs.systemdService ];
            serviceConfig.Type = "oneshot";
            serviceConfig.RuntimeDirectory = serviceName caCfg.systemdService;
            # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
            script = ''
              cd $RUNTIME_DIRECTORY

              cat >ca.template <<EOF
              organization = "${caCfg.name}"
              cn = "${caCfg.name}"
              expiration_days = 365
              ca
              cert_signing_key
              crl_signing_key
              EOF

              keyfile="${caCfg.paths.key}"
              keydir="$(dirname -- "$keyfile")"
              mkdir -p "$keydir"
              [ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
                --generate-privkey \
                --key-type rsa \
                --sec-param High \
                --outfile "$keyfile"
              chmod 666 "$keyfile"

              certfile="${caCfg.paths.cert}"
              certdir="$(dirname -- "$certfile")"
              mkdir -p "$certdir"
              [ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
                --generate-self-signed \
                --load-privkey "$keyfile" \
                --template ca.template \
                --outfile "$certfile"
              chmod 666 "$certfile"
            '';
          }
        ) cfg.cas.selfsigned;
      }
      # Config for self-signed CA bundle.
      {
        systemd.services.${serviceName config.shb.certs.systemdService} = (
          lib.mkIf (cfg.cas.selfsigned != { }) {
            wantedBy = [ "multi-user.target" ];
            serviceConfig.Type = "oneshot";
            script = ''
              mkdir -p /etc/ssl/certs

              # This file is automatically idempotent since the source files used are idempotent.

              rm -f /etc/ssl/certs/ca-bundle.crt
              rm -f /etc/ssl/certs/ca-certificates.crt

              cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-bundle.crt
              cat /etc/static/ssl/certs/ca-bundle.crt > /etc/ssl/certs/ca-certificates.crt
              for file in ${
                lib.concatStringsSep " " (mapAttrsToList (_name: caCfg: caCfg.paths.cert) cfg.cas.selfsigned)
              }; do
                  cat "$file" >> /etc/ssl/certs/ca-bundle.crt
                  cat "$file" >> /etc/ssl/certs/ca-certificates.crt
              done
            '';
          }
        );
      }
      # Config for self-signed cert.
      {
        systemd.services = lib.mapAttrs' (
          _name: certCfg:
          lib.nameValuePair (serviceName certCfg.systemdService) {
            after = [ certCfg.ca.systemdService ];
            requires = [ certCfg.ca.systemdService ];
            wantedBy = [ "multi-user.target" ];
            serviceConfig.RuntimeDirectory = serviceName certCfg.systemdService;
            # Taken from https://github.com/NixOS/nixpkgs/blob/7f311dd9226bbd568a43632c977f4992cfb2b5c8/nixos/tests/custom-ca.nix
            script =
              let
                extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
                chmod = cert: ''
                  chown root:${certCfg.group} "${cert}"
                  chmod 640 "${cert}"
                '';
              in
              ''
                cd $RUNTIME_DIRECTORY

                # server cert template
                cat >server.template <<EOF
                organization = "An example company"
                cn = "${certCfg.domain}"
                expiration_days = 30
                dns_name = "${certCfg.domain}"
                ${extraDnsNames}
                encryption_key
                signing_key
                EOF

                keyfile="${certCfg.paths.key}"
                keydir="$(dirname -- "$keyfile")"
                mkdir -p "$keydir"
                [ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
                  --generate-privkey \
                  --key-type rsa \
                  --sec-param High \
                  --outfile "$keyfile"
                ${chmod "$keyfile"}

                certfile="${certCfg.paths.cert}"
                certdir="$(dirname -- "$certfile")"
                mkdir -p "$certdir"
                [ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
                  --generate-certificate \
                  --load-privkey "$keyfile" \
                  --load-ca-privkey "${certCfg.ca.paths.key}" \
                  --load-ca-certificate "${certCfg.ca.paths.cert}" \
                  --template server.template \
                  --outfile "$certfile"
                ${chmod "$certfile"}
              '';

            postStart = lib.optionalString (certCfg.reloadServices != [ ]) ''
              systemctl --no-block try-reload-or-restart ${lib.escapeShellArgs certCfg.reloadServices}
            '';

            serviceConfig.Type = "oneshot";
            # serviceConfig.User = "nextcloud";
          }
        ) cfg.certs.selfsigned;
      }
      # Config for Let's Encrypt cert.
      {
        users.users = lib.mkMerge (
          mapAttrsToList (name: certCfg: {
            ${certCfg.makeAvailableToUser}.extraGroups = lib.mkIf (!(isNull certCfg.makeAvailableToUser)) [
              config.security.acme.defaults.group
            ];
          }) cfg.certs.letsencrypt
        );

        security.acme.acceptTerms = lib.mkIf (cfg.certs.letsencrypt != { }) true;

        security.acme.certs =
          let
            extraDomainsCfg =
              certCfg:
              map (name: {
                "${name}" = {
                  email = certCfg.adminEmail;
                  enableDebugLogs = certCfg.debug;
                  server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
                };
              }) certCfg.extraDomains;
          in
          lib.mkMerge (
            flatten (
              mapAttrsToList (
                name: certCfg:
                [
                  {
                    "${name}" = {
                      extraDomainNames = certCfg.extraDomains;
                      email = certCfg.adminEmail;
                      enableDebugLogs = certCfg.debug;
                      server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
                    }
                    // lib.optionalAttrs (certCfg.dnsProvider != null) {
                      inherit (certCfg) dnsProvider dnsResolver;
                      inherit (certCfg) group reloadServices;
                      environmentFile = certCfg.credentialsFile;
                    };
                  }
                ]
                ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
              ) cfg.certs.letsencrypt
            )
          );

        services.nginx =
          let
            extraDomainsCfg =
              extraDomains:
              map (name: {
                virtualHosts."${name}" = {
                  # addSSL = true;
                  enableACME = true;
                };
              }) extraDomains;
          in
          lib.mkMerge (
            flatten (
              mapAttrsToList (
                name: certCfg:
                lib.optionals (certCfg.dnsProvider == null) (
                  [
                    {
                      virtualHosts."${name}" = {
                        # addSSL = true;
                        enableACME = true;
                      };
                    }
                  ]
                  ++ extraDomainsCfg certCfg.extraDomains
                )
              ) cfg.certs.letsencrypt
            )
          );

        systemd.services =
          let
            extraDomainsCfg =
              certCfg:
              flatten (
                map (
                  name:
                  lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
                    {
                      "acme-${name}".environment = certCfg.additionalEnvironment;
                    }
                  ]
                  ++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
                    {
                      "acme-${name}" = {
                        after = certCfg.afterAndWants;
                        wants = certCfg.afterAndWants;
                      };
                    }
                  ]
                ) certCfg.extraDomains
              );
          in
          lib.mkMerge (
            flatten (
              mapAttrsToList (
                name: certCfg:
                lib.optionals (certCfg.additionalEnvironment != { } && certCfg.dnsProvider == null) [
                  {
                    "acme-${certCfg.domain}".environment = certCfg.additionalEnvironment;
                  }
                ]
                ++ lib.optionals (certCfg.afterAndWants != [ ] && certCfg.dnsProvider == null) [
                  {
                    "acme-${certCfg.domain}" = {
                      after = certCfg.afterAndWants;
                      wants = certCfg.afterAndWants;
                    };
                  }
                ]
                ++ lib.optionals (certCfg.dnsProvider == null) (extraDomainsCfg certCfg)
              ) cfg.certs.letsencrypt
            )
          );

        services.prometheus.exporters.node-cert = optionalAttrs (cfg.certs.letsencrypt != { }) {
          enable = true;
          listenAddress = "127.0.0.1";
          user = "acme";
          paths =
            let
              pathCfg =
                name: certCfg:
                let
                  mainDomainPaths = map dirOf [
                    certCfg.paths.cert
                    certCfg.paths.key
                  ];
                  # Not sure this will work for all cases.
                  mainPath = dirOf (dirOf certCfg.paths.cert);
                  extraDomainsPath = map (x: "${mainPath}/${x}") certCfg.extraDomains;
                in
                mainDomainPaths ++ extraDomainsPath;
            in
            unique (flatten (mapAttrsToList pathCfg cfg.certs.letsencrypt));
        };

        services.prometheus.scrapeConfigs =
          let
            scrapeCfg = name: certCfg: [
              {
                job_name = "node-cert-${name}";
                static_configs = [
                  {
                    targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node-cert.port}" ];
                    labels = {
                      "hostname" = config.networking.hostName;
                      "domain" = certCfg.domain;
                    };
                  }
                ];
              }
            ];
          in
          optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
      }
      (lib.mkIf (cfg.enableDashboard && (cfg.certs.selfsigned != { } || cfg.certs.letsencrypt != { })) {
        shb.monitoring.dashboards = [
          ./ssl/dashboard/SSL.json
        ];
      })
    ];
}
