{
  config,
  pkgs,
  lib,
  utils,
  shb,
  ...
}:

let
  cfg = config.shb.borgbackup;

  inherit (lib)
    concatStringsSep
    filterAttrs
    flatten
    literalExpression
    optionals
    listToAttrs
    mapAttrsToList
    mkOption
    mkMerge
    ;
  inherit (lib)
    mkIf
    nameValuePair
    optionalAttrs
    removePrefix
    ;
  inherit (lib.types)
    attrsOf
    int
    oneOf
    nonEmptyStr
    nullOr
    str
    submodule
    ;

  commonOptions =
    {
      name,
      prefix,
      config,
      ...
    }:
    {
      enable = lib.mkEnableOption ''
        SelfHostBlocks' BorgBackup block;

        A disabled instance will not backup data anymore
        but still provides the helper tool to restore snapshots
      '';

      passphrase = lib.mkOption {
        description = "Encryption key for the backup repository.";
        type = lib.types.submodule {
          options = shb.contracts.secret.mkRequester {
            mode = "0400";
            owner = config.request.user;
            ownerText = "[shb.borgbackup.${prefix}.<name>.request.user](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.request.user)";
            restartUnits = [ (fullName name config.settings.repository) ];
            restartUnitsText = "[ [shb.borgbackup.${prefix}.<name>.settings.repository](#blocks-borgbackup-options-shb.borgbackup.${prefix}._name_.settings.repository) ]";
          };
        };
      };

      repository = lib.mkOption {
        description = "Repository to send the backups to.";
        type = submodule {
          options = {
            path = mkOption {
              type = str;
              description = "Repository location";
            };

            secrets = mkOption {
              type = attrsOf shb.secretFileType;
              default = { };
              description = ''
                Secrets needed to access the repository where the backups will be stored.

                See [s3 config](https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html#amazon-s3) for an example
                and [list](https://restic.readthedocs.io/en/latest/040_backup.html#environment-variables) for the list of all secrets.

              '';
              example = literalExpression ''
                {
                  AWS_ACCESS_KEY_ID.source = <path/to/secret>;
                  AWS_SECRET_ACCESS_KEY.source = <path/to/secret>;
                }
              '';
            };

            timerConfig = mkOption {
              type = attrsOf utils.systemdUtils.unitOptions.unitOption;
              default = {
                OnCalendar = "daily";
                Persistent = true;
              };
              description = "When to run the backup. See {manpage}`systemd.timer(5)` for details.";
              example = {
                OnCalendar = "00:05";
                RandomizedDelaySec = "5h";
                Persistent = true;
              };
            };
          };
        };
      };

      retention = lib.mkOption {
        description = "Retention options. See {command}`borg help prune` for the available options.";
        type = attrsOf (oneOf [
          int
          nonEmptyStr
        ]);
        default = {
          within = "1d";
          hourly = 24;
          daily = 7;
          weekly = 4;
          monthly = 6;
        };
      };

      consistency = lib.mkOption {
        description = "Consistency frequency options.";
        type = lib.types.attrsOf lib.types.nonEmptyStr;
        default = { };
        example = {
          repository = "2 weeks";
          archives = "1 month";
        };
      };

      limitUploadKiBs = mkOption {
        type = nullOr int;
        description = "Limit upload bandwidth to the given KiB/s amount.";
        default = null;
        example = 8000;
      };

      stateDir = mkOption {
        type = nullOr lib.types.str;
        description = ''
          Override the directory in which {command}`borg` stores its
          configuration and cache. By default it uses the user's
          home directory but is some cases this can cause conflicts.
        '';
        default = null;
      };
    };

  repoSlugName = name: builtins.replaceStrings [ "/" ":" ] [ "_" "_" ] (removePrefix "/" name);
  fullName = name: repository: "borgbackup-job-${name}_${repoSlugName repository.path}";
in
{
  imports = [
    ../../lib/module.nix
    ../blocks/monitoring.nix
  ];

  options.shb.borgbackup = {
    enableDashboard = lib.mkEnableOption "the Backups SHB dashboard" // {
      default = true;
    };

    instances = mkOption {
      description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
      default = { };
      type = attrsOf (
        submodule (
          { name, config, ... }:
          {
            options = shb.contracts.backup.mkProvider {
              settings = mkOption {
                description = ''
                  Settings specific to the BorgBackup provider.
                '';

                type = submodule {
                  options = commonOptions {
                    inherit name config;
                    prefix = "instances";
                  };
                };
              };

              resultCfg = {
                restoreScript = fullName name config.settings.repository;
                restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";

                backupService = "${fullName name config.settings.repository}.service";
                backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
              };
            };
          }
        )
      );
    };

    databases = mkOption {
      description = "Databases to backup following the [database backup contract](./shb.contracts-databasebackup.html).";
      default = { };
      type = attrsOf (
        submodule (
          { name, config, ... }:
          {
            options = shb.contracts.databasebackup.mkProvider {
              settings = mkOption {
                description = ''
                  Settings specific to the BorgBackup provider.
                '';

                type = submodule {
                  options = commonOptions {
                    inherit name config;
                    prefix = "databases";
                  };
                };
              };

              resultCfg = {
                restoreScript = fullName name config.settings.repository;
                restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";

                backupService = "${fullName name config.settings.repository}.service";
                backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
              };
            };
          }
        )
      );
    };

    borgServer = lib.mkOption {
      description = "Add borgbackup package to `environment.systemPackages` so external backups can use this server as a remote.";
      default = false;
      example = true;
      type = lib.types.bool;
    };

    # Taken from https://github.com/HubbeKing/restic-kubernetes/blob/73bfbdb0ba76939a4c52173fa2dbd52070710008/README.md?plain=1#L23
    performance = lib.mkOption {
      description = "Reduce performance impact of backup jobs.";
      default = { };
      type = lib.types.submodule {
        options = {
          niceness = lib.mkOption {
            type = lib.types.ints.between (-20) 19;
            description = "nice priority adjustment, defaults to 15 for ~20% CPU time of normal-priority process";
            default = 15;
          };
          ioSchedulingClass = lib.mkOption {
            type = lib.types.enum [
              "idle"
              "best-effort"
              "realtime"
            ];
            description = "ionice scheduling class, defaults to best-effort IO.";
            default = "best-effort";
          };
          ioPriority = lib.mkOption {
            type = lib.types.nullOr (lib.types.ints.between 0 7);
            description = "ionice priority, defaults to 7 for lowest priority IO.";
            default = 7;
          };
        };
      };
    };
  };

  config = lib.mkIf (cfg.instances != { } || cfg.databases != { }) (
    let
      enabledInstances = filterAttrs (k: i: i.settings.enable) cfg.instances;
      enabledDatabases = filterAttrs (k: i: i.settings.enable) cfg.databases;
    in
    lib.mkMerge [
      {
        environment.systemPackages =
          optionals (cfg.borgServer || enabledInstances != { } || enabledDatabases != { })
            [
              pkgs.borgbackup
            ];
      }
      {
        services.borgbackup.jobs =
          let
            mkJob = name: instance: {
              "${name}_${repoSlugName instance.settings.repository.path}" = {
                inherit (instance.request) user;

                repo = instance.settings.repository.path;

                paths = instance.request.sourceDirectories;

                encryption.mode = "repokey-blake2";
                # We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
                encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";

                doInit = true;
                failOnWarnings = true;
                stateDir = instance.settings.stateDir;

                persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
                startAt = ""; # Some non-empty string value tricks the upstream module in creating the systemd timer.

                prune.keep = instance.settings.retention;

                preHook = concatStringsSep "\n" instance.request.hooks.beforeBackup;

                postHook = concatStringsSep "\n" instance.request.hooks.afterBackup;

                extraArgs = (
                  optionals (instance.settings.limitUploadKiBs != null) [
                    "--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
                  ]
                );

                exclude = instance.request.excludePatterns;
              };
            };
          in
          mkMerge (mapAttrsToList mkJob enabledInstances);
      }
      {
        services.borgbackup.jobs =
          let
            mkJob = name: instance: {
              "${name}_${repoSlugName instance.settings.repository.path}" = {
                inherit (instance.request) user;

                repo = instance.settings.repository.path;

                dumpCommand = lib.getExe (
                  pkgs.writeShellApplication {
                    name = "dump-command";
                    text = instance.request.backupCmd;
                  }
                );

                encryption.mode = "repokey-blake2";
                # We do not set encryption.passphrase here, we set BORG_PASSPHRASE_FD further down.
                encryption.passCommand = "cat ${instance.settings.passphrase.result.path}";

                doInit = true;
                failOnWarnings = true;
                stateDir = instance.settings.stateDir;

                persistentTimer = instance.settings.repository.timerConfig.Persistent or false;
                startAt = ""; # Some non-empty list value that tricks upstream in creating the systemd timer.

                prune.keep = instance.settings.retention;

                extraArgs = (
                  optionals (instance.settings.limitUploadKiBs != null) [
                    "--upload-ratelimit=${toString instance.settings.limitUploadKiBs}"
                  ]
                );
              };
            };
          in
          mkMerge (mapAttrsToList mkJob enabledDatabases);
      }
      {
        systemd.timers =
          let
            mkTimer = name: instance: {
              ${fullName name instance.settings.repository} = {
                timerConfig = lib.mkForce instance.settings.repository.timerConfig;
              };
            };
          in
          mkMerge (mapAttrsToList mkTimer (enabledInstances // enabledDatabases));
      }
      {
        systemd.services =
          let
            mkSettings =
              name: instance:
              let
                serviceName = fullName name instance.settings.repository;
              in
              {
                ${serviceName} = mkMerge [
                  {
                    serviceConfig = {
                      # Purposely not a oneshot systemd service otherwise
                      # the service waits on the completion the backup before deactivating.
                      # This seems like a nice property at first but there is one annoying
                      # edge case when deploying. If a backup job somehow is started when
                      # the deploy happens, the deploy will wait on the service to finish
                      # before considering the deploy done. And worse, it will consider the
                      # deploy as failed if the backup fails, which is not what you want.
                      Nice = lib.mkForce cfg.performance.niceness;
                      IOSchedulingClass = lib.mkForce cfg.performance.ioSchedulingClass;
                      IOSchedulingPriority = lib.mkForce cfg.performance.ioPriority;
                      # BindReadOnlyPaths = instance.sourceDirectories;
                    };
                  }
                  (optionalAttrs (instance.settings.repository.secrets != { }) {
                    serviceConfig.EnvironmentFile = [
                      "/run/secrets_borgbackup/${serviceName}"
                    ];
                    after = [ "${serviceName}-pre.service" ];
                    requires = [ "${serviceName}-pre.service" ];
                  })
                ];

                "${serviceName}-pre" = mkIf (instance.settings.repository.secrets != { }) (
                  let
                    script = shb.genConfigOutOfBandSystemd {
                      config = instance.settings.repository.secrets;
                      configLocation = "/run/secrets_borgbackup/${serviceName}";
                      generator = shb.toEnvVar;
                      user = instance.request.user;
                    };
                  in
                  {
                    script = script.preStart;
                    # Makes the systemd service wait for the backup to be done before changing state to inactive.
                    serviceConfig.Type = "oneshot";
                    serviceConfig.LoadCredential = script.loadCredentials;
                  }
                );
              };
          in
          mkMerge (flatten (mapAttrsToList mkSettings (enabledInstances // enabledDatabases)));
      }
      {
        systemd.services =
          let
            mkEnv =
              name: instance:
              nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
                enable = true;
                wantedBy = [ "multi-user.target" ];
                serviceConfig.Type = "oneshot";
                script = (
                  shb.replaceSecrets {
                    userConfig = instance.settings.repository.secrets // {
                      BORG_PASSCOMMAND = ''"cat ${instance.settings.passphrase.result.path}"'';
                      BORG_REPO = instance.settings.repository.path;
                    };
                    resultPath = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
                    generator = shb.toEnvVar;
                    user = instance.request.user;
                  }
                );
              };
          in
          listToAttrs (flatten (mapAttrsToList mkEnv (cfg.instances // cfg.databases)));
      }
      {
        environment.systemPackages =
          let
            mkBorgBackupBinary =
              name: instance:
              shb.contracts.backup.mkRestoreScript {
                name = fullName name instance.settings.repository;
                user = instance.request.user;
                sudoPreserveEnvs = [
                  "BORG_REPO"
                  "BORG_PASSCOMMAND"
                ];
                secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
                restoreCmd = ''(cd / && ${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\")'';
                listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
              };
          in
          flatten (mapAttrsToList mkBorgBackupBinary cfg.instances);
      }
      {
        environment.systemPackages =
          let
            mkBorgBackupBinary =
              name: instance:
              shb.contracts.backup.mkRestoreScript {
                name = fullName name instance.settings.repository;
                user = instance.request.user;
                sudoPreserveEnvs = [
                  "BORG_REPO"
                  "BORG_PASSCOMMAND"
                ];
                secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
                restoreCmd = ''${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\" --stdout | ${instance.request.restoreCmd}'';
                listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
              };
          in
          flatten (mapAttrsToList mkBorgBackupBinary cfg.databases);
      }

      (lib.mkIf (cfg.enableDashboard && (cfg.instances != { } || cfg.databases != { })) {
        shb.monitoring.dashboards = [
          ./backup/dashboard/Backups.json
        ];
      })
    ]
  );
}
