{ 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 ? {}
   , ...
   }@primary:
    ''
    info "generateKey uid=\"${uid}\""
    if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
     then
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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 --fixed-list-mode --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
    ;
  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
      ${pkgs.pass}/bin/pass "${primary.passPath}" |
      ${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"
     then
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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
      ${pkgs.pass}/bin/pass "${passPath}" |
      ${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}";
  signer = s: if s == null
              then ""
              else ''--sign --default-key "${refKey s}"'';
  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 "");

  passOfFingerprint = key:
    # Return shell code
    # which fills a map from the fingerprints of the given key
    # to its password file.
    ''
    # shell.gnupg.pass.passOfFingerprint
    for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
     do eval "pass_$fpr=\"${key.passPath}\""
     done
    '';
  forgetPass =
    # Return shell code
    # which installs an exit and keyboard interruption (^C) trap
    # removing any pass from gpg-agent
    # whose keygrip is registered in $keygrips.
    ''
    # forgetPass
    keygrips=
    forgetPass () {
      for keygrip in $keygrips
       do
        echo >&2 "gpg: forget: keygrip=$keygrip"
        GNUPGHOME=${gnupg.gnupgHome} \
        ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
        true
       done
      keygrips=
     }
    trap 'forgetPass' EXIT INT
    '';
  presetPass = keys: uid:
    # Return shell code
    # which preset the pass of given uid into gpg-agent,
    # using keys to find where the pass is stored.
    ''
    ${unlines (map passOfFingerprint keys)}
    # presetPass
    GNUPGHOME=${gnupg.gnupgHome} \
    ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
    ${head1}
    fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
    eval pass="\''${pass_$fpr}"
    if test -n "$pass"
     then
      for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
       do
        keygrips="$keygrips $keygrip"
        echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
        ${pkgs.pass}/bin/pass "$pass" |
        GNUPGHOME=${gnupg.gnupgHome} \
        ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip
       done
     fi
    '';

  # 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 --fixed-list-mode --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 --fixed-list-mode --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 --fixed-list-mode \
     --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.path;
    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 ({uid, ...}: {
      #config.uid = lib.mkDefault uid;
      options = {
        uid = lib.mkOption {
          type        = types.str;
          example     = "John Doe <john.doe@example.coop>";
          default     = uid;
          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     = "1y";
          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     = "1y";
                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.
          '';
        };
      };
    }));
  };
  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;
    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:-tty}" 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
      fixed-list-mode
      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.
    '';
  };
};
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)
  '';
};
}