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

with lib;

let
  cfg = config.services.cfssl;
in {
  options.services.cfssl = {
    enable = mkEnableOption "the CFSSL CA api-server";

    dataDir = mkOption {
      default = "/var/lib/cfssl";
      type = types.path;
      description = ''
        The work directory for CFSSL.

        ::: {.note}
        If left as the default value this directory will automatically be
        created before the CFSSL server starts, otherwise you are
        responsible for ensuring the directory exists with appropriate
        ownership and permissions.
        :::
      '';
    };

    address = mkOption {
      default = "127.0.0.1";
      type = types.str;
      description = "Address to bind.";
    };

    port = mkOption {
      default = 8888;
      type = types.port;
      description = "Port to bind.";
    };

    ca = mkOption {
      defaultText = literalExpression ''"''${cfg.dataDir}/ca.pem"'';
      type = types.str;
      description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'.";
    };

    caKey = mkOption {
      defaultText = literalExpression ''"file:''${cfg.dataDir}/ca-key.pem"'';
      type = types.str;
      description = "CA private key -- accepts '[file:]fname' or 'env:varname'.";
    };

    caBundle = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Path to root certificate store.";
    };

    intBundle = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Path to intermediate certificate store.";
    };

    intDir = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Intermediates directory.";
    };

    metadata = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = ''
        Metadata file for root certificate presence.
        The content of the file is a json dictionary (k,v): each key k is
        a SHA-1 digest of a root certificate while value v is a list of key
        store filenames.
      '';
    };

    remote = mkOption {
      default = null;
      type = types.nullOr types.str;
      description = "Remote CFSSL server.";
    };

    configFile = mkOption {
      default = null;
      type = types.nullOr types.str;
      description = "Path to configuration file. Do not put this in nix-store as it might contain secrets.";
    };

    responder = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Certificate for OCSP responder.";
    };

    responderKey = mkOption {
      default = null;
      type = types.nullOr types.str;
      description = "Private key for OCSP responder certificate. Do not put this in nix-store.";
    };

    tlsKey = mkOption {
      default = null;
      type = types.nullOr types.str;
      description = "Other endpoint's CA private key. Do not put this in nix-store.";
    };

    tlsCert = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Other endpoint's CA to set up TLS protocol.";
    };

    mutualTlsCa = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Mutual TLS - require clients be signed by this CA.";
    };

    mutualTlsCn = mkOption {
      default = null;
      type = types.nullOr types.str;
      description = "Mutual TLS - regex for whitelist of allowed client CNs.";
    };

    tlsRemoteCa = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "CAs to trust for remote TLS requests.";
    };

    mutualTlsClientCert = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Mutual TLS - client certificate to call remote instance requiring client certs.";
    };

    mutualTlsClientKey = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Mutual TLS - client key to call remote instance requiring client certs. Do not put this in nix-store.";
    };

    dbConfig = mkOption {
      default = null;
      type = types.nullOr types.path;
      description = "Certificate db configuration file. Path must be writeable.";
    };

    logLevel = mkOption {
      default = 1;
      type = types.enum [ 0 1 2 3 4 5 ];
      description = "Log level (0 = DEBUG, 5 = FATAL).";
    };
  };

  config = mkIf cfg.enable {
    users.groups.cfssl = {
      gid = config.ids.gids.cfssl;
    };

    users.users.cfssl = {
      description = "cfssl user";
      home = cfg.dataDir;
      group = "cfssl";
      uid = config.ids.uids.cfssl;
    };

    systemd.services.cfssl = {
      description = "CFSSL CA API server";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];

      serviceConfig = lib.mkMerge [
        {
          WorkingDirectory = cfg.dataDir;
          Restart = "always";
          User = "cfssl";
          Group = "cfssl";

          ExecStart = with cfg; let
            opt = n: v: optionalString (v != null) ''-${n}="${v}"'';
          in
            lib.concatStringsSep " \\\n" [
              "${pkgs.cfssl}/bin/cfssl serve"
              (opt "address" address)
              (opt "port" (toString port))
              (opt "ca" ca)
              (opt "ca-key" caKey)
              (opt "ca-bundle" caBundle)
              (opt "int-bundle" intBundle)
              (opt "int-dir" intDir)
              (opt "metadata" metadata)
              (opt "remote" remote)
              (opt "config" configFile)
              (opt "responder" responder)
              (opt "responder-key" responderKey)
              (opt "tls-key" tlsKey)
              (opt "tls-cert" tlsCert)
              (opt "mutual-tls-ca" mutualTlsCa)
              (opt "mutual-tls-cn" mutualTlsCn)
              (opt "mutual-tls-client-key" mutualTlsClientKey)
              (opt "mutual-tls-client-cert" mutualTlsClientCert)
              (opt "tls-remote-ca" tlsRemoteCa)
              (opt "db-config" dbConfig)
              (opt "loglevel" (toString logLevel))
            ];
        }
        (mkIf (cfg.dataDir == options.services.cfssl.dataDir.default) {
          StateDirectory = baseNameOf cfg.dataDir;
          StateDirectoryMode = 700;
        })
      ];
    };

    services.cfssl = {
      ca = mkDefault "${cfg.dataDir}/ca.pem";
      caKey = mkDefault "${cfg.dataDir}/ca-key.pem";
    };
  };
}
