{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    literalExpression
    mkEnableOption
    mkOption
    mkIf
    types
    ;

  cfg = config.programs.bat;

  toConfigFile =
    attrs:
    let
      inherit (builtins) isBool attrNames;
      nonBoolFlags = lib.filterAttrs (_: v: !(isBool v)) attrs;
      enabledBoolFlags = lib.filterAttrs (_: v: isBool v && v) attrs;

      keyValuePairs = lib.generators.toKeyValue {
        mkKeyValue = k: v: "--${k}=${lib.escapeShellArg v}";
        listsAsDuplicateKeys = true;
      } nonBoolFlags;
      switches = lib.concatMapStrings (k: ''
        --${k}
      '') (attrNames enabledBoolFlags);
    in
    keyValuePairs + switches;
in
{
  meta.maintainers = with lib.maintainers; [ khaneliman ];

  options.programs.bat = {
    enable = mkEnableOption "bat, a cat clone with wings";

    config = mkOption {
      type =
        with types;
        attrsOf (oneOf [
          str
          (listOf str)
          bool
        ]);
      default = { };
      example = {
        theme = "TwoDark";
        pager = "less -FR";
        map-syntax = [
          "*.jenkinsfile:Groovy"
          "*.props:Java Properties"
        ];
      };
      description = ''
        Bat configuration.
      '';
    };

    extraPackages = mkOption {
      type = types.listOf types.package;
      default = [ ];
      example = literalExpression "with pkgs.bat-extras; [ batdiff batman batgrep batwatch ];";
      description = ''
        Additional bat packages to install.
      '';
    };

    package = lib.mkPackageOption pkgs "bat" { };

    themes = mkOption {
      type = types.attrsOf (
        types.either types.lines (
          types.submodule {
            options = {
              src = mkOption {
                type = types.path;
                description = "Path to the theme folder.";
              };

              file = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Subpath of the theme file within the source, if needed.";
              };
            };
          }
        )
      );
      default = { };
      example = literalExpression ''
        {
          dracula = {
            src = pkgs.fetchFromGitHub {
              owner = "dracula";
              repo = "sublime"; # Bat uses sublime syntax for its themes
              rev = "26c57ec282abcaa76e57e055f38432bd827ac34e";
              sha256 = "019hfl4zbn4vm4154hh3bwk6hm7bdxbr1hdww83nabxwjn99ndhv";
            };
            file = "Dracula.tmTheme";
          };
        }
      '';
      description = ''
        Additional themes to provide.
      '';
    };

    syntaxes = mkOption {
      type = types.attrsOf (
        types.either types.lines (
          types.submodule {
            options = {
              src = mkOption {
                type = types.path;
                description = "Path to the syntax folder.";
              };
              file = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Subpath of the syntax file within the source, if needed.";
              };
            };
          }
        )
      );
      default = { };
      example = literalExpression ''
        {
          gleam = {
            src = pkgs.fetchFromGitHub {
              owner = "molnarmark";
              repo = "sublime-gleam";
              rev = "2e761cdb1a87539d827987f997a20a35efd68aa9";
              hash = "sha256-Zj2DKTcO1t9g18qsNKtpHKElbRSc9nBRE2QBzRn9+qs=";
            };
            file = "syntax/gleam.sublime-syntax";
          };
        }
      '';
      description = ''
        Additional syntaxes to provide.
      '';
    };
  };

  config = mkIf cfg.enable (
    lib.mkMerge [
      (mkIf (lib.any lib.isString (lib.attrValues cfg.themes)) {
        warnings = [
          ''
            Using programs.bat.themes as a string option is deprecated and will be
            removed in the future. Please change to using it as an attribute set
            instead.
          ''
        ];
      })
      (mkIf (lib.any lib.isString (lib.attrValues cfg.syntaxes)) {
        warnings = [
          ''
            Using programs.bat.syntaxes as a string option is deprecated and will be
            removed in the future. Please change to using it as an attribute set
            instead.
          ''
        ];
      })
      {
        home.packages = [ cfg.package ] ++ cfg.extraPackages;

        xdg.configFile = lib.mkMerge (
          [
            {
              "bat/config" = mkIf (cfg.config != { }) { text = toConfigFile cfg.config; };
            }
          ]
          ++ (lib.flip lib.mapAttrsToList cfg.themes (
            name: val: {
              "bat/themes/${name}.tmTheme" =
                if lib.isString val then
                  {
                    text = val;
                  }
                else
                  {
                    source = if isNull val.file then "${val.src}" else "${val.src}/${val.file}";
                  };
            }
          ))
          ++ (lib.flip lib.mapAttrsToList cfg.syntaxes (
            name: val: {
              "bat/syntaxes/${name}.sublime-syntax" =
                if lib.isString val then
                  {
                    text = val;
                  }
                else
                  {
                    source = if isNull val.file then "${val.src}" else "${val.src}/${val.file}";
                  };
            }
          ))
        );

        # NOTE: run `bat cache --build` in an empty directory to work
        # around failure when ~/cache exists
        # https://github.com/sharkdp/bat/issues/1726
        home.activation.batCache = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
          (
            export XDG_CACHE_HOME=${lib.escapeShellArg config.xdg.cacheHome}
            verboseEcho "Rebuilding bat theme cache"
            cd "${pkgs.emptyDirectory}"
            run ${lib.getExe cfg.package} cache --build
          )
        '';
      }
    ]
  );
}
