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

let
  cfg = config.programs.passwordManager;
  passmenuScript = pkgs.writeShellScriptBin "passmenu-bemenu" (builtins.readFile ./passmenu); # TODO: override original passmenu script coming from pass itself
  passff-host = pkgs.passff-host;

  inherit (lib)
    mkDefault
    mkEnableOption
    mkIf
    mkOption
    mkOverride
    types
    ;
in
{
  options.programs.passwordManager = {
    enable = mkEnableOption "password manager using pass, passmenu, and passff.";
    length = mkOption {
      type = types.str;
      default = "20";
      description = "Default length for generated passwords.";
    };
    charset = mkOption {
      type = types.enum [
        "alphanumerical"
        "ascii"
        "custom"
      ];
      default = "alphanumerical";
      description = ''
        Character set for generated passwords. "alphanumerical" and "ascii" will get overwritten by the corresponding charset. Anything else will get parsed as the charset directly.
      '';
    };
    editor = mkOption {
      type = types.str;
      default = "nvim";
      description = "Editor to use for editing passwords. Make sure it is installed on your system.";
    };
    wayland = mkOption {
      type = types.bool;
      default = false;
      description = "If true, bemenu and ydotool will be used instead of dmenu and xdotool.";
    };
    key = mkOption {
      type = types.str;
      default = "";
      description = "GPG key for password store.";
    };
  };

  config = mkIf cfg.enable {
    programs.password-store = {
      enable = true;
      package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]);
      settings = {
        PASSWORD_STORE_DIR = mkDefault "${config.xdg.dataHome}/password-store";
        PASSWORD_STORE_KEY = mkIf (cfg.key != "") cfg.key;
        PASSWORD_STORE_GENERATED_LENGTH = cfg.length;
        PASSWORD_STORE_CHARACTER_SET =
          if cfg.charset == "alphanumerical" then
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
          else if cfg.charset == "ascii" then
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:',.<>/?"
          else
            cfg.charset;
        PASSWORD_STORE_ENABLE_EXTENSIONS = "true";
        EDITOR = cfg.editor;
      };
    };

    services.gpg-agent.pinentry.package = mkOverride 1001 pkgs.pinentry-qt; # mkDefault collides with gpg home module

    home.packages = [
      passmenuScript
      pkgs.zbar
    ]
    ++ (
      if cfg.wayland then
        [
          pkgs.bemenu
          pkgs.ydotool
        ]
      else
        [
          pkgs.dmenu
          pkgs.xdotool
        ]
    );

    home.sessionVariables.PASSWORD_STORE_MENU = if cfg.wayland then "bemenu" else "dmenu";

    # FIXME: passff does not autofill OTPs
    programs.librewolf = mkIf config.programs.librewolf.enable {
      package = pkgs.librewolf.override {
        nativeMessagingHosts = [
          passff-host
        ];
        hasMozSystemDirPatch = true;
      };
      nativeMessagingHosts = [ passff-host ];
      profiles.default.extensions.packages =
        with inputs.nur.legacyPackages."${pkgs.stdenv.hostPlatform.system}".repos.rycee.firefox-addons; [
          passff
        ];
    };
  };
}
