{ config, lib, pkgs, ...}:
with lib;
let
  cfg = config.services.duplicity;
  stateDirectory = "/var/lib/duplicity";
  localTarget = if hasPrefix "file://" cfg.targetUrl
    then removePrefix "file://" cfg.targetUrl else null;
in {
  options.services.duplicity = {
    enable = mkEnableOption "backups with duplicity";
    root = mkOption {
      type = types.path;
      default = "/";
      description = ''
        Root directory to backup.
      '';
    };
    include = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "/home" ];
      description = ''
        List of paths to include into the backups. See the FILE SELECTION
        section in duplicity
        1 for details on the syntax.
      '';
    };
    exclude = mkOption {
      type = types.listOf types.str;
      default = [];
      description = ''
        List of paths to exclude from backups. See the FILE SELECTION section in
        duplicity
        1 for details on the syntax.
      '';
    };
    targetUrl = mkOption {
      type = types.str;
      example = "s3://host:port/prefix";
      description = ''
        Target url to backup to. See the URL FORMAT section in
        duplicity
        1 for supported urls.
      '';
    };
    secretFile = mkOption {
      type = types.nullOr types.path;
      default = null;
      description = ''
        Path of a file containing secrets (gpg passphrase, access key...) in
        the format of EnvironmentFile as described by
        systemd.exec
        5. For example:
        
        PASSPHRASE=...
        AWS_ACCESS_KEY_ID=...
        AWS_SECRET_ACCESS_KEY=...
        
      '';
    };
    frequency = mkOption {
      type = types.nullOr types.str;
      default = "daily";
      description = ''
        Run duplicity with the given frequency (see
        systemd.time
        7 for the format).
        If null, do not run automatically.
      '';
    };
    extraFlags = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "--full-if-older-than" "1M" ];
      description = ''
        Extra command-line flags passed to duplicity. See
        duplicity
        1.
      '';
    };
  };
  config = mkIf cfg.enable {
    systemd = {
      services.duplicity = {
        description = "backup files with duplicity";
        environment.HOME = stateDirectory;
        serviceConfig = {
          ExecStart = ''
            ${pkgs.duplicity}/bin/duplicity ${escapeShellArgs (
              [
                cfg.root
                cfg.targetUrl
                "--archive-dir" stateDirectory
              ]
              ++ concatMap (p: [ "--include" p ]) cfg.include
              ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
              ++ cfg.extraFlags)}
          '';
          PrivateTmp = true;
          ProtectSystem = "strict";
          ProtectHome = "read-only";
          StateDirectory = baseNameOf stateDirectory;
        } // optionalAttrs (localTarget != null) {
          ReadWritePaths = localTarget;
        } // optionalAttrs (cfg.secretFile != null) {
          EnvironmentFile = cfg.secretFile;
        };
      } // optionalAttrs (cfg.frequency != null) {
        startAt = cfg.frequency;
      };
      tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
    };
    assertions = singleton {
      # Duplicity will fail if the last file selection option is an include. It
      # is not always possible to detect but this simple case can be caught.
      assertion = cfg.include != [] -> cfg.exclude != [] || cfg.extraFlags != [];
      message = ''
        Duplicity will fail if you only specify included paths ("Because the
        default is to include all files, the expression is redundant. Exiting
        because this probably isn't what you meant.")
      '';
    };
  };
}