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

let
  cfg = config.shb.certs;

  contracts = pkgs.callPackage ../contracts {};
in
{
  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";
    };
    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 = contracts.ssl.certs-paths;
            default = rec {
              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 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 = contracts.ssl.certs-paths;
            default = rec {
              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 = 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
      filterProvider = provider: lib.attrsets.filterAttrs (k: i: i.provider == provider);

      serviceName = lib.strings.removeSuffix ".service";
    in
      lib.mkMerge [
        # 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

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

                mkdir -p "$(dirname -- "${caCfg.paths.cert}")"
                ${pkgs.gnutls}/bin/certtool         \
                  --generate-self-signed            \
                  --load-privkey ${caCfg.paths.key} \
                  --template ca.template            \
                  --outfile ${caCfg.paths.cert}
                chmod 666 ${caCfg.paths.cert}
              '';
            }
          ) 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

            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 " " (lib.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

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

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

              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 (lib.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 (lib.flatten (lib.mapAttrsToList (name: certCfg:
            [{
              "${name}" = {
                extraDomainNames = [ certCfg.domain ] ++ 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;
                credentialsFile = 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 (lib.flatten (lib.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: lib.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 (lib.flatten (lib.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));
        }
      ];
}