{ 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 <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> 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
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> 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
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry> 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
        <citerefentry><refentrytitle>systemd.exec</refentrytitle>
        <manvolnum>5</manvolnum></citerefentry>. For example:
        <programlisting>
        PASSPHRASE=<replaceable>...</replaceable>
        AWS_ACCESS_KEY_ID=<replaceable>...</replaceable>
        AWS_SECRET_ACCESS_KEY=<replaceable>...</replaceable>
        </programlisting>
      '';
    };

    frequency = mkOption {
      type = types.nullOr types.str;
      default = "daily";
      description = ''
        Run duplicity with the given frequency (see
        <citerefentry><refentrytitle>systemd.time</refentrytitle>
        <manvolnum>7</manvolnum></citerefentry> 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
        <citerefentry><refentrytitle>duplicity</refentrytitle>
        <manvolnum>1</manvolnum></citerefentry>.
      '';
    };
  };

  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.")
      '';
    };
  };
}