{ pkgs, lib, config, ... }:
let
  inherit (lib) types;
  inherit (config) gnupg;
  unlines = builtins.concatStringsSep "\n";
  unwords = builtins.concatStringsSep " ";

  generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
  generateKey =
    _uid:
    { uid ? uid
    , algo ? "future-default"
    , usage ? [ "default" ]
    , expire ? "-"
    , passPath
    , subKeys ? { }
    , postRun ? ""
    , ...
    }@primary:
    ''
      info "generateKey uid=\"${uid}\""
      if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
       then
        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
        ${gpg-with-home}/bin/gpg-with-home \
          --batch --pinentry-mode loopback --passphrase-fd 0 \
          --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
       fi
      ${head1}
      fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
      caps=$(${gpg-with-home}/bin/gpg-with-home \
              --with-colons --with-fingerprint \
              --list-secret-keys -- "=${uid}" |
             ${pkgs.gnugrep}/bin/grep '^ssb:' |
             ${pkgs.coreutils}/bin/cut -d : -f 12 || true)
    ''
    + unlines (map (generateSubKey primary) subKeys)
    + generateBackupKey "$fpr" primary
    + postRun
  ;
  generateSubKey =
    primary:
    { expire ? primary.expire
    , algo ? primary.algo
    , usage
    , ...
    }:
    ''
      info "  generateSubKey usage=[${unwords usage}]"
      if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
       then
        ${if primary.passPath != "" then "${pkgs.pass}/bin/pass '${primary.passPath}'" else "cat /dev/null"} |
        ${gpg-with-home}/bin/gpg-with-home \
          --batch --pinentry-mode loopback --passphrase-fd 0 \
          --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
       fi
    '';
  generateBackupKey =
    fpr:
    { passPath
    , backupRecipients ? [ ]
    , uid
    , ...
    }:
    lib.optionalString (backupRecipients != [ ])
      ''
        info "  generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
        mkdir -p "${gnupg.gnupgHome}/backup/${uid}/"
        if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc"
         then
          ${gpg-with-home}/bin/gpg-with-home \
            --batch \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
            --export-options export-backup \
            --export "${fpr}"
         fi
      '' + (if backupRecipients == [ "" ] then
      ''
        if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" &&
         ${gpg-with-home}/bin/gpg-with-home --list-secret-keys "${fpr}" | grep -q "sec "
         then
          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
          ${gpg-with-home}/bin/gpg-with-home \
            --pinentry-mode loopback --passphrase-fd 0 \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \
            --gen-revoke "${fpr}"
         fi
        if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
         then
          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
          ${gpg-with-home}/bin/gpg-with-home \
            --batch --pinentry-mode loopback --passphrase-fd 0 \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \
            --export-options export-backup \
            --export-secret-key "${fpr}"
         fi
        if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
         then
          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
          ${gpg-with-home}/bin/gpg-with-home \
            --batch --pinentry-mode loopback --passphrase-fd 0 \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \
            --export-options export-backup \
            --export-secret-subkeys "${fpr}"
         fi
      '' else ''
      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
       then
        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
        ${gpg-with-home}/bin/gpg-with-home \
          --pinentry-mode loopback --passphrase-fd 0 \
          --armor --gen-revoke "${fpr}" |
        gpg --encrypt ${recipients backupRecipients} \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
       fi
      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
       then
        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
        ${gpg-with-home}/bin/gpg-with-home \
          --batch --pinentry-mode loopback --passphrase-fd 0 \
          --armor --export-options export-backup \
          --export-secret-key "${fpr}" |
        gpg --encrypt ${recipients backupRecipients} \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
       fi
      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
       then
        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
        ${gpg-with-home}/bin/gpg-with-home \
          --batch --pinentry-mode loopback --passphrase-fd 0 \
          --armor --export-options export-backup \
          --export-secret-subkeys "${fpr}" |
        gpg --encrypt ${recipients backupRecipients} \
            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
       fi
    '');
  recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs);
  refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}";
  lettersKeyUsage = usage:
    (if builtins.elem "encrypt" usage then "e" else "") +
    (if builtins.elem "sign" usage then "s" else "") +
    (if builtins.elem "cert" usage then "c" else "") +
    (if builtins.elem "auth" usage then "a" else "");

  # Initialize the keyring according to gnupg.keys.
  gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
    set -eu
    set -o pipefail
    ${info}
  '' +
  generateKeys gnupg.keys
  );

  # A wrapper around gpg to set GNUPGHOME.
  gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
    GNUPGHOME=${gnupg.gnupgHome} \
    exec ${pkgs.gnupg}/bin/gpg "$@"
  '';

  # A wrapper around gpg to get fingerprints.
  gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
    set -eu
    ${gpg-with-home}/bin/gpg-with-home \
     --with-colons --with-fingerprint --with-subkey-fingerprint \
     --list-public-keys "$@" |
    while IFS=: read -r t x x x key x x x x uid x
     do case $t in
       (pub|sub|sec|ssb)
        while IFS=: read -r t x x x x x x x x fpr x
         do case $t in (fpr) printf '%s\n' "$fpr"; break;;
         esac done
        ;;
     esac done
  '';

  # A wrapper around gpg to get keygrips.
  gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
    set -eu
    ${gpg-with-home}/bin/gpg-with-home \
     --with-colons --with-keygrip \
     --list-public-keys "$@" |
    while IFS=: read -r t x x x key x x x x uid x do case $t in (pub|sub|sec|ssb) while IFS=: read -r t x x x x x x x x grp x do case $t in (grp) printf '%s\n' "$grp"; break;; esac done ;; esac done
  '';

  # A wrapper around gpg to get uids.
  gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
    set -eu
    ${gpg-with-home}/bin/gpg-with-home \
     --with-colons \
     --list-public-keys "$@" |
    while IFS=: read -r t st x x x x x id x uid x
     do case $t in
       (uid)
        case $st in
         (u) printf '%s\n' "$uid";;
         esac
        ;;
     esac done
  '';

  head1 = ''
    head1(){
      IFS= read -r line
      cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
      printf %s "$line"
    }
  '';
  info = ''
    info(){
      echo >&2 "gpg-init: $*"
    }
  '';
in
{
  options.gnupg = {
    enable = lib.mkEnableOption "GnuPG shell utilities";
    gnupgHome = lib.mkOption {
      type = types.str;
      default = "sec/gnupg";
      description = ''
    '';
    };
    keys = lib.mkOption {
      default = { };
      example =
        {
          "John Doe. <contact@example.coop>" = {
            algo = "rsa4096";
            expire = "1y";
            usage = [ "cert" "sign" ];
            passPath = "example.coop/gpg/contact";
            subKeys = [
              { algo = "rsa4096"; expire = "1y"; usage = [ "sign" ]; }
              { algo = "rsa4096"; expire = "1y"; usage = [ "encrypt" ]; }
              { algo = "rsa4096"; expire = "1y"; usage = [ "auth" ]; }
            ];
            backupRecipients = [ "@john@doe.pro" ];
          };
        };
      type = types.attrsOf (types.submodule ({ name, ... }: {
        options = {
          uid = lib.mkOption {
            type = types.str;
            example = "John Doe <john.doe@example.coop>";
            default = name;
            description = ''
              User ID.
            '';
          };
          algo = lib.mkOption {
            type = types.enum [ "rsa4096" ];
            default = "future-default";
            example = "rsa4096";
            description = ''
              Cryptographic algorithm.
            '';
          };
          expire = lib.mkOption {
            type = types.str;
            default = "0";
            example = "1y";
            description = ''
              Expiration timeout.
            '';
          };
          usage = lib.mkOption {
            type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
            default = [ "default" ];
            example = [ "cert" "sign" "encrypt" "auth" ];
            description = ''
              Cryptographic usage.
            '';
          };
          passPath = lib.mkOption {
            type = types.str;
            example = "gnupg/coop/example/contact@";
            description = ''
              Password path.
            '';
          };
          subKeys = lib.mkOption {
            type = types.listOf (types.submodule {
              options = {
                algo = lib.mkOption {
                  type = types.enum [ "rsa4096" ];
                  default = "default";
                  example = "rsa4096";
                  description = ''
                    Cryptographic algorithm.
                  '';
                };
                expire = lib.mkOption {
                  type = types.str;
                  default = "0";
                  example = "1y";
                  description = ''
                    Expiration timeout.
                  '';
                };
                usage = lib.mkOption {
                  type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
                  default = [ "default" ];
                  example = [ "sign" "encrypt" "auth" ];
                  description = ''
                    Cryptographic usage.
                  '';
                };
              };
            });
          };
          backupRecipients = lib.mkOption {
            type = with types; listOf str;
            default = [ ];
            example = [ "@john@doe.pro" ];
            description = ''
              Backup keys used to encrypt the a backup copy of the secret keys.
            '';
          };
          postRun = lib.mkOption {
            type = types.lines;
            default = "";
            description = ''
              Shell code to run after the key has been generated or tested to exist.
            '';
          };
        };
      }));
    };
    dirmngrConf = lib.mkOption {
      type = types.lines;
      apply = s: pkgs.writeText "dirmngr.conf" s;
      default = ''
        allow-ocsp
        hkp-cacert ${gnupg.keyserverPEM}
        keyserver hkps://keys.mayfirst.org
        #use-tor
        #log-file ${gnupg.gnupgHome}/dirmngr.log
        #standard-resolver
      '';
      description = ''
        GnuPG's dirmngr.conf content.
      '';
    };
    keyserverPEM = lib.mkOption {
      type = types.lines;
      apply = s: pkgs.writeText "keyserver.pem" s;
      default = builtins.readFile gnupg/keyserver.pem;
      description = ''
        dirmngr's hkp-cacert content.
      '';
    };
    gpgAgentConf = lib.mkOption {
      type = types.lines;
      apply = s: pkgs.writeText "gpg-agent.conf" (s + "\n" + gnupg.gpgAgentExtraConf);
      default =
        let
          pinentry = pkgs.writeShellScript "pinentry" ''
            #!${pkgs.runtimeShell}
            # choose pinentry depending on PINENTRY_USER_DATA
            # this *only works* with gpg2
            # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020
            case "''${PINENTRY_USER_DATA:-curses}" in
            curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";;
            #emacs)  exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";;
            #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";;
            gtk-2)  exec ''${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";;
            none)   exit 1;; # do not ask for passphrase
            #qt)     exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";;
            tty)    exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";;
            esac
          '';
        in
        ''
          allow-loopback-pinentry
          allow-preset-passphrase
          default-cache-ttl 17200
          default-cache-ttl-ssh 17200
          enable-ssh-support
          max-cache-ttl 17200
          max-cache-ttl-ssh 17200
          no-allow-external-cache
          pinentry-program ${pinentry}
        '';
      description = ''
        GnuPG's gpg-agent.conf content.
      '';
    };
    gpgConf = lib.mkOption {
      type = types.lines;
      apply = s: pkgs.writeText "gpg.conf" (s + "\n" + gnupg.gpgExtraConf);
      default = ''
        auto-key-locate keyserver
        cert-digest-algo SHA512
        charset utf-8
        default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
        keyid-format 0xlong
        keyserver-options no-honor-keyserver-url
        no-auto-key-locate
        no-default-keyring
        no-emit-version
        personal-cipher-preferences AES256 AES CAST5
        personal-digest-preferences SHA512
        quiet
        s2k-cipher-algo AES256
        s2k-count 65536
        s2k-digest-algo SHA512
        s2k-mode 3
        tofu-default-policy unknown
        trust-model tofu+pgp
        use-agent
        utf8-strings
      '';
      description = ''
        GnuPG's gpg.conf content.
      '';
    };
    gpgExtraConf = lib.mkOption {
      type = types.lines;
      default = "";
      description = ''
        GnuPG's gpg.conf extra content.
      '';
    };
    gpgAgentExtraConf = lib.mkOption {
      type = types.lines;
      default = "";
      description = ''
        GnuPG's gpg-agent.conf extra content.
      '';
    };
  };
  config = lib.mkIf gnupg.enable {
    nix-shell.buildInputs = [
      gpg-with-home
      gpg-fingerprint
      gpg-keygrip
      gpg-uid
      gpg-init
    ];
    nix-shell.shellHook = ''
      # gnupg
      ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
      ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf}      ${gnupg.gnupgHome}/gpg.conf
      ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
      ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf}  ${gnupg.gnupgHome}/dirmngr.conf
      export GNUPGHOME=${gnupg.gnupgHome}
      install -dm700 "$GNUPGHOME"
      export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
      ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
      export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
    '';
  };
}