{ ... }:
{
  _class = "clan.service";
  manifest.name = "borgbackup";
  manifest.description = "Efficient, deduplicating backup program with optional compression and secure encryption.";
  manifest.categories = [ "System" ];
  manifest.readme = builtins.readFile ./README.md;

  # TODO: a client can only be in one instance, add constraint

  roles.server = {

    interface =
      { lib, ... }:
      {

        options.directory = lib.mkOption {
          type = lib.types.str;
          default = "/var/lib/borgbackup";
          description = ''
            The directory where the borgbackup repositories are stored.
          '';
        };

      };

    perInstance =
      {
        roles,
        settings,
        ...
      }:
      {
        nixosModule =
          {
            config,
            ...
          }:
          {

            config.services.openssh.enable = true;

            config.services.borgbackup.repos =
              let
                borgbackupIpMachinePath =
                  machine:
                  config.clan.core.settings.directory
                  + "/vars/per-machine/${machine}/borgbackup/borgbackup.ssh.pub/value";

                hosts = builtins.mapAttrs (machineName: _machineSettings: {
                  # name = "${instanceName}-${machineName}";
                  # value = {
                  path = "${settings.directory}/${machineName}";
                  authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machineName)) ];
                  # };
                  # }) machinesWithKey;
                }) roles.client.machines;
              in
              hosts;
          };
      };
  };

  roles.client = {
    interface =
      {
        lib,
        ...
      }:
      {

        options.destinations = lib.mkOption {
          type = lib.types.attrsOf (
            lib.types.submodule (
              { name, ... }:
              {
                options = {
                  name = lib.mkOption {
                    type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
                    default = name;
                    description = "the name of the backup job";
                  };
                  repo = lib.mkOption {
                    type = lib.types.str;
                    description = "the borgbackup repository to backup to";
                  };
                  rsh = lib.mkOption {
                    type = lib.types.str;
                    defaultText = "ssh -i \${config.clan.core.vars.generators.borgbackup.files.\"borgbackup.ssh\".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
                    description = "the rsh to use for the backup";
                  };
                };
              }
            )
          );
          default = { };
          description = ''
            external destinations where the machine should be backuped to
          '';
        };

        options.exclude = lib.mkOption {
          type = lib.types.listOf lib.types.str;
          example = [ "*.pyc" ];
          default = [ ];
          description = ''
            Directories/Files to exclude from the backup.
            Use * as a wildcard.
          '';
        };
      };

    perInstance =
      {
        extendSettings,
        roles,
        ...
      }:
      {
        nixosModule =
          {
            config,
            lib,
            pkgs,
            ...
          }:
          let
            settings = extendSettings {

              # Adding default value with option merging, because it depends on
              # generators, which we can reference here.
              options.destinations = lib.mkOption {
                type = lib.types.attrsOf (
                  lib.types.submodule {
                    options = {
                      rsh = lib.mkOption {
                        default = "ssh -i ${
                          config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
                        } -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes -o PasswordAuthentication=no";
                      };
                    };
                  }
                );
              };
            };

          in

          {
            config =
              let
                preBackupScript = ''
                  declare -A preCommandErrors

                  ${lib.concatMapStringsSep "\n" (
                    state:
                    lib.optionalString (state.preBackupCommand != null) ''
                      echo "Running pre-backup command for ${state.name}"
                      if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
                        preCommandErrors["${state.name}"]=1
                      fi
                    ''
                  ) (lib.attrValues config.clan.core.state)}

                  if [[ ''${#preCommandErrors[@]} -gt 0 ]]; then
                    echo "pre-backup commands failed for the following services:"
                    for state in "''${!preCommandErrors[@]}"; do
                      echo "  $state"
                    done
                    exit 1
                  fi
                '';

                # The destinations from server.roles.machines.*
                # name is the server, machine can only be in one instance
                internalDestinations =
                  let
                    destinations = builtins.map (serverName: {
                      name = "${serverName}";
                      value = {
                        # inherit name;
                        name = "${serverName}";
                        repo = "borg@${serverName}:.";
                        # rsh = "";

                        rsh = "ssh -i ${
                          config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
                        } -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes";
                      };
                    }) (builtins.attrNames roles.server.machines);
                  in
                  (builtins.listToAttrs destinations);

                # The destinations specified via roles.client.machines.*.settings.destinations.<name>
                # name is the <name>
                externalDestinations = lib.mapAttrs' (
                  name: dest: lib.nameValuePair name dest
                ) settings.destinations;

                allDestinations =
                  lib.warnIf ((builtins.intersectAttrs externalDestinations internalDestinations) != { })
                    "You are overwriting an internalDestinations through an externalDestination configuration."
                    (internalDestinations // externalDestinations);

              in
              {
                services.openssh.enable = true;

                # Derived from the destinations
                systemd.services = lib.mapAttrs' (
                  destName: _dest:
                  lib.nameValuePair "borgbackup-job-${destName}" {
                    # since borgbackup mounts the system read-only, we need to
                    # run in a ExecStartPre script, so we can generate
                    # additional files.
                    serviceConfig.ExecStartPre = [
                      ''+${pkgs.writeShellScript "borgbackup-job-${destName}-pre-backup-commands" preBackupScript}''
                    ];
                  }
                ) allDestinations;

                services.borgbackup.jobs = lib.mapAttrs (_: dest: {
                  paths = lib.unique (
                    lib.flatten (map (state: state.folders) (lib.attrValues config.clan.core.state))
                  );
                  exclude = settings.exclude;
                  repo = dest.repo;
                  environment.BORG_RSH = dest.rsh;
                  compression = "auto,zstd";
                  startAt = "*-*-* 01:00:00";
                  persistentTimer = true;

                  encryption = {
                    mode = "repokey";
                    passCommand = "cat ${config.clan.core.vars.generators.borgbackup.files."borgbackup.repokey".path}";
                  };

                  prune.keep = {
                    within = "1d"; # Keep all archives from the last day
                    daily = 7;
                    weekly = 4;
                    monthly = 0;
                  };
                }) allDestinations;

                clan.core.vars.generators.borgbackup = {

                  files."borgbackup.ssh.pub".secret = false;
                  files."borgbackup.ssh" = { };
                  files."borgbackup.repokey" = { };

                  migrateFact = "borgbackup";
                  runtimeInputs = [
                    pkgs.coreutils
                    pkgs.openssh
                    pkgs.xkcdpass
                  ];
                  script = ''
                    ssh-keygen -t ed25519 -N "" -f "$out"/borgbackup.ssh
                    xkcdpass -n 4 -d - > "$out"/borgbackup.repokey
                  '';
                };

                clan.core.backups.providers.borgbackup = {
                  list = "borgbackup-list";
                  create = "borgbackup-create";
                  restore = "borgbackup-restore";
                };

                environment.systemPackages = [
                  (pkgs.writeShellApplication {
                    name = "borgbackup-create";
                    runtimeInputs = [ config.systemd.package ];
                    text = ''
                      ${lib.concatMapStringsSep "\n" (dest: ''
                        systemctl start borgbackup-job-${dest}
                      '') (lib.attrNames allDestinations)}
                    '';
                  })
                  (pkgs.writeShellApplication {
                    name = "borgbackup-list";
                    runtimeInputs = [ pkgs.jq ];
                    text = ''
                      (${
                        lib.concatMapStringsSep "\n" (
                          dest:
                          # we need yes here to skip the changed url verification
                          ''echo y | /run/current-system/sw/bin/borg-job-${dest.name} list --json | jq '[.archives[] | {"name": ("${dest.name}::${dest.repo}::" + .name)}]' ''
                        ) (lib.attrValues allDestinations)
                      }) | jq -s 'add // []'
                    '';
                  })
                  (pkgs.writeShellApplication {
                    name = "borgbackup-restore";
                    runtimeInputs = [ pkgs.gawk ];
                    text = ''
                      cd /
                      IFS=':' read -ra FOLDER <<< "''${FOLDERS-}"
                      job_name=$(echo "$NAME" | awk -F'::' '{print $1}')
                      backup_name=''${NAME#"$job_name"::}
                      if [[ ! -x /run/current-system/sw/bin/borg-job-"$job_name" ]]; then
                        echo "borg-job-$job_name not found: Backup name is invalid" >&2
                        exit 1
                      fi
                      echo y | /run/current-system/sw/bin/borg-job-"$job_name" extract "$backup_name" "''${FOLDER[@]}"
                    '';
                  })
                ];
              };
          };
      };
  };
}
