{ ... }:
{
  _class = "clan.service";
  manifest.name = "localbackup";
  manifest.description = "Automatically backups current machine to local directory.";
  manifest.categories = [ "System" ];
  manifest.readme = builtins.readFile ./README.md;

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

        options = {

          targets = 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";
                    };
                    directory = lib.mkOption {
                      type = lib.types.str;
                      description = "the directory to backup";
                    };
                    mountpoint = lib.mkOption {
                      type = lib.types.nullOr lib.types.str;
                      default = null;
                      description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
                    };
                    preMountHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run before the directory is mounted";
                    };
                    postMountHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run after the directory is mounted";
                    };
                    preUnmountHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run before the directory is unmounted";
                    };
                    postUnmountHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run after the directory is unmounted";
                    };
                    preBackupHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run before the backup";
                    };
                    postBackupHook = lib.mkOption {
                      type = lib.types.nullOr lib.types.lines;
                      default = null;
                      description = "Shell commands to run after the backup";
                    };
                  };
                }
              )
            );
            # default = { };
            description = "List of directories where backups are stored";
          };

          snapshots = lib.mkOption {
            type = lib.types.int;
            default = 20;
            description = "Number of snapshots to keep";
          };
        };

      };

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

          let
            mountHook = target: ''
              if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
                /run/current-system/sw/bin/localbackup-mount-${target.name}
              fi
              if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
                trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
              fi
            '';

            uniqueFolders = lib.unique (
              lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
            );

            rsnapshotConfig = target: ''
              config_version	1.2
              snapshot_root	${target.directory}
              sync_first	1
              cmd_cp	${pkgs.coreutils}/bin/cp
              cmd_rm	${pkgs.coreutils}/bin/rm
              cmd_rsync	${pkgs.rsync}/bin/rsync
              cmd_ssh	${pkgs.openssh}/bin/ssh
              cmd_logger	${pkgs.inetutils}/bin/logger
              cmd_du	${pkgs.coreutils}/bin/du
              cmd_rsnapshot_diff	${pkgs.rsnapshot}/bin/rsnapshot-diff

              ${lib.optionalString (target.postBackupHook != null) ''
                cmd_postexec	${pkgs.writeShellScript "postexec.sh" ''
                  set -efu -o pipefail
                  ${target.postBackupHook}
                ''}
              ''}
              retain	snapshot	${builtins.toString settings.snapshots}
              ${lib.concatMapStringsSep "\n" (folder: ''
                backup	${folder}	${config.networking.hostName}/
              '') uniqueFolders}
            '';
          in

          {

            environment.systemPackages = [
              (pkgs.writeShellScriptBin "localbackup-create" ''
                set -efu -o pipefail
                export PATH=${
                  lib.makeBinPath [
                    pkgs.rsnapshot
                    pkgs.coreutils
                    pkgs.util-linux
                  ]
                }
                ${lib.concatMapStringsSep "\n" (target: ''
                  ${mountHook target}
                  echo "Creating backup '${target.name}'"

                  ${lib.optionalString (target.preBackupHook != null) ''
                    (
                      ${target.preBackupHook}
                    )
                  ''}

                  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
                    ''
                  ) (builtins.attrValues config.clan.core.state)}

                  rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
                  rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
                '') (builtins.attrValues settings.targets)}'')
              (pkgs.writeShellScriptBin "localbackup-list" ''
                set -efu -o pipefail
                export PATH=${
                  lib.makeBinPath [
                    pkgs.jq
                    pkgs.findutils
                    pkgs.coreutils
                    pkgs.util-linux
                  ]
                }
                (${
                  lib.concatMapStringsSep "\n" (target: ''
                    (
                      ${mountHook target}
                      find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
                        | jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
                    )
                  '') (builtins.attrValues settings.targets)
                }) | jq -s .
              '')
              (pkgs.writeShellScriptBin "localbackup-restore" ''
                set -efu -o pipefail
                export PATH=${
                  lib.makeBinPath [
                    pkgs.rsync
                    pkgs.coreutils
                    pkgs.util-linux
                    pkgs.gawk
                  ]
                }
                if [[ "''${NAME:-}" == "" ]]; then
                  echo "No backup name given via NAME environment variable"
                  exit 1
                fi
                if [[ "''${FOLDERS:-}" == "" ]]; then
                  echo "No folders given via FOLDERS environment variable"
                  exit 1
                fi
                name=$(awk -F'::' '{print $1}' <<< $NAME)
                backupname=''${NAME#$name::}

                if command -v localbackup-mount-$name; then
                  localbackup-mount-$name
                fi
                if command -v localbackup-unmount-$name; then
                  trap "localbackup-unmount-$name" EXIT
                fi

                if [[ ! -d $backupname ]]; then
                  echo "No backup found $backupname"
                  exit 1
                fi

                IFS=':' read -ra FOLDER <<< "''$FOLDERS"
                for folder in "''${FOLDER[@]}"; do
                  mkdir -p "$folder"
                  rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
                done
              '')
            ]
            ++ (lib.mapAttrsToList (
              name: target:
              pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
                set -efu -o pipefail
                ${lib.optionalString (target.preMountHook != null) target.preMountHook}
                ${lib.optionalString (target.mountpoint != null) ''
                  if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
                    ${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
                  fi
                ''}
                ${lib.optionalString (target.postMountHook != null) target.postMountHook}
              ''
            ) settings.targets)
            ++ lib.mapAttrsToList (
              name: target:
              pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
                set -efu -o pipefail
                ${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
                ${lib.optionalString (
                  target.mountpoint != null
                ) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
                ${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
              ''
            ) settings.targets;

            clan.core.backups.providers.localbackup = {
              # TODO list needs to run locally or on the remote machine
              list = "localbackup-list";
              create = "localbackup-create";
              restore = "localbackup-restore";
            };

          };
      };
  };
}
