{ config, lib, pkgs, ... }:
with config.krebs.lib;
let
  out = {
    options.krebs.backup = api;
    config = lib.mkIf cfg.enable imp;
  };

  cfg = config.krebs.backup;

  api = {
    enable = mkEnableOption "krebs.backup" // { default = true; };
    plans = mkOption {
      default = {};
      type = types.attrsOf (types.submodule ({ config, ... }: {
        options = {
          enable = mkEnableOption "krebs.backup.${config.name}" // {
            default = true;
          };
          method = mkOption {
            type = types.enum ["pull" "push"];
          };
          name = mkOption {
            type = types.str;
            default = config._module.args.name;
          };
          src = mkOption {
            type = types.krebs.file-location;
          };
          dst = mkOption {
            type = types.krebs.file-location;
          };
          startAt = mkOption {
            default = "hourly";
            type = with types; nullOr str; # TODO systemd.time(7)'s calendar event
          };
          snapshots = mkOption {
            default = {
              hourly   = { format = "%Y-%m-%dT%H"; retain =  4; };
              daily    = { format = "%Y-%m-%d";    retain =  7; };
              weekly   = { format = "%YW%W";       retain =  4; };
              monthly  = { format = "%Y-%m";       retain = 12; };
              yearly   = { format = "%Y";                       };
            };
            type = types.attrsOf (types.submodule {
              options = {
                format = mkOption {
                  type = types.str; # TODO date's +FORMAT
                };
                retain = mkOption {
                  type = types.nullOr types.int;
                  default = null; # null = retain all snapshots
                };
              };
            });
          };
        };
      }));
    };
  };

  imp = {
    systemd.services =
      listToAttrs (map (plan: nameValuePair "backup.${plan.name}" {
        # TODO if there is plan.user, then use its privkey
        # TODO push destination users need a similar path
        path = with pkgs; [
          coreutils
          gnused
          openssh
          rsync
          utillinux
        ];
        serviceConfig = rec {
          ExecStart = start plan;
          SyslogIdentifier = ExecStart.name;
          Type = "oneshot";
        };
        startAt = mkIf (plan.startAt != null) plan.startAt;
      }) (filter (plan: build-host-is "pull" "dst" plan ||
                        build-host-is "push" "src" plan)
                 enabled-plans));

    users.groups.backup.gid = genid "backup";
    users.users.root.openssh.authorizedKeys.keys =
      map (plan: getAttr plan.method {
        push = plan.src.host.ssh.pubkey;
        pull = plan.dst.host.ssh.pubkey;
      }) (filter (plan: build-host-is "pull" "src" plan ||
                        build-host-is "push" "dst" plan)
                 enabled-plans);
  };

  enabled-plans = filter (getAttr "enable") (attrValues cfg.plans);

  build-host-is = method: side: plan:
    plan.method == method &&
    config.krebs.build.host.name == plan.${side}.host.name;

  start = plan: pkgs.writeDash "backup.${plan.name}" ''
    set -efu
    ${getAttr plan.method {
      push = ''
        identity=${shell.escape plan.src.host.ssh.privkey.path}
        src_path=${shell.escape plan.src.path}
        src=$src_path
        dst_user=root
        dst_host=$(${fastest-address plan.dst.host})
        dst_port=$(${network-ssh-port plan.dst.host "$dst_host"})
        dst_path=${shell.escape plan.dst.path}
        dst=$dst_user@$dst_host:$dst_path
        echo "update snapshot: current; $src -> $dst" >&2
        dst_shell() {
          exec ssh -F /dev/null \
              -i "$identity" \
              ''${dst_port:+-p $dst_port} \
              "$dst_user@$dst_host" \
              -T "$with_dst_path_lock_script"
        }
        rsh="ssh -F /dev/null -i $identity ''${dst_port:+-p $dst_port}"
        local_rsync() {
          rsync "$@"
        }
        remote_rsync=${shell.escape (concatStringsSep " && " [
          "mkdir -m 0700 -p ${shell.escape plan.dst.path}/current"
          "exec flock -n ${shell.escape plan.dst.path} rsync"
        ])}
      '';
      pull = ''
        identity=${shell.escape plan.dst.host.ssh.privkey.path}
        src_user=root
        src_host=$(${fastest-address plan.src.host})
        src_port=$(${network-ssh-port plan.src.host "$src_host"})
        src_path=${shell.escape plan.src.path}
        src=$src_user@$src_host:$src_path
        dst_path=${shell.escape plan.dst.path}
        dst=$dst_path
        echo "update snapshot: current; $dst <- $src" >&2
        dst_shell() {
          eval "$with_dst_path_lock_script"
        }
        rsh="ssh -F /dev/null -i $identity ''${src_port:+-p $src_port}"
        local_rsync() {
          mkdir -m 0700 -p ${shell.escape plan.dst.path}/current
          flock -n ${shell.escape plan.dst.path} rsync "$@"
        }
        remote_rsync=rsync
      '';
    }}
    # Note that this only works because we trust date +%s to produce output
    # that doesn't need quoting when used to generate a command string.
    # TODO relax this requirement by selectively allowing to inject variables
    #   e.g.: ''${shell.quote "exec env NOW=''${shell.unquote "$NOW"} ..."}
    with_dst_path_lock_script="exec env start_date=$(date +%s) "${shell.escape
      "flock -n ${shell.escape plan.dst.path} /bin/sh"
    }
    local_rsync >&2 \
        -aAXF --delete \
        --rsh="$rsh" \
        --rsync-path="$remote_rsync" \
        --link-dest="$dst_path/current" \
        "$src/" \
        "$dst/.partial"
    dst_shell < ${toFile "backup.${plan.name}.take-snapshots" ''
      set -efu
      : $start_date

      dst=${shell.escape plan.dst.path}

      mv "$dst/current" "$dst/.previous"
      mv "$dst/.partial" "$dst/current"
      rm -fR "$dst/.previous"
      echo >&2

      snapshot() {(
        : $ns $format $retain
        name=$(date --date="@$start_date" +"$format")
        if ! test -e "$dst/$ns/$name"; then
          echo >&2 "create snapshot: $ns/$name"
          mkdir -m 0700 -p "$dst/$ns"
          rsync >&2 \
              -aAXF --delete \
              --link-dest="$dst/current" \
              "$dst/current/" \
              "$dst/$ns/.partial.$name"
          mv "$dst/$ns/.partial.$name" "$dst/$ns/$name"
          echo >&2
        fi
        case $retain in
          ([0-9]*)
            delete_from=$(($retain + 1))
            ls -r "$dst/$ns" \
              | sed -n "$delete_from,\$p" \
              | while read old_name; do
                  echo >&2 "delete snapshot: $ns/$old_name"
                  rm -fR "$dst/$ns/$old_name"
                done
            ;;
          (ALL)
            :
            ;;
        esac
      )}

      ${concatStringsSep "\n" (mapAttrsToList (ns: { format, retain, ... }:
        toString (map shell.escape [
          "ns=${ns}"
          "format=${format}"
          "retain=${if retain == null then "ALL" else toString retain}"
          "snapshot"
        ]))
        plan.snapshots)}
    ''}
  '';

  # XXX Is one ping enough to determine fastest address?
  fastest-address = host: ''
    { ${pkgs.fping}/bin/fping </dev/null -a \
        ${concatMapStringsSep " " shell.escape
          (mapAttrsToList (_: net: head net.aliases) host.nets)} \
      | ${pkgs.coreutils}/bin/head -1; }
  '';

  # Note that we don't escape word on purpose, so we can deref shell vars.
  # TODO type word
  network-ssh-port = host: word: ''
    case ${word} in
    ${concatStringsSep ";;\n" (mapAttrsToList
      (_: net: "(${head net.aliases}) echo ${toString net.ssh.port}")
      host.nets)};;
    esac
  '';

in out
# TODO ionice
# TODO mail on failed push, pull
# TODO mail on missing push
# TODO don't cancel plans on activation
#   also, don't hang while deploying at:
#   starting the following units: backup.wu-home-xu.push.service, backup.wu-home-xu.push.timer
# TODO make sure /bku is properly mounted
# TODO make sure that secure hosts cannot backup to insecure ones
# TODO optionally only backup when src and dst are near enough :)
# TODO try using btrfs for snapshots (configurable)
# TODO warn if partial snapshots are found
# TODO warn if unknown stuff is found in dst path