{
  config,
  pkgs,
  lib,
  ...
}:

with lib;

let
  cfg = config.services.snapper;

  mkValue =
    v:
    if isList v then
      "\"${
        concatMapStringsSep " " (escape [
          "\\"
          " "
        ]) v
      }\""
    else if v == true then
      "yes"
    else if v == false then
      "no"
    else if isString v then
      "\"${v}\""
    else
      builtins.toJSON v;

  mkKeyValue = k: v: "${k}=${mkValue v}";

  # "it's recommended to always specify the filesystem type"  -- man snapper-configs
  defaultOf = k: if k == "FSTYPE" then null else configOptions.${k}.default or null;

  safeStr = types.strMatching "[^\n\"]*" // {
    description = "string without line breaks or quotes";
    descriptionClass = "conjunction";
  };

  configOptions = {
    SUBVOLUME = mkOption {
      type = types.path;
      description = ''
        Path of the subvolume or mount point.
        This path is a subvolume and has to contain a subvolume named
        .snapshots.
        See also man:snapper(8) section PERMISSIONS.
      '';
    };

    FSTYPE = mkOption {
      type = types.enum [ "btrfs" ];
      default = "btrfs";
      description = ''
        Filesystem type. Only btrfs is stable and tested.
      '';
    };

    ALLOW_GROUPS = mkOption {
      type = types.listOf safeStr;
      default = [ ];
      description = ''
        List of groups allowed to operate with the config.

        Also see the PERMISSIONS section in man:snapper(8).
      '';
    };

    ALLOW_USERS = mkOption {
      type = types.listOf safeStr;
      default = [ ];
      example = [ "alice" ];
      description = ''
        List of users allowed to operate with the config. "root" is always
        implicitly included.

        Also see the PERMISSIONS section in man:snapper(8).
      '';
    };

    TIMELINE_CLEANUP = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Defines whether the timeline cleanup algorithm should be run for the config.
      '';
    };

    TIMELINE_CREATE = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Defines whether hourly snapshots should be created.
      '';
    };

    TIMELINE_LIMIT_HOURLY = mkOption {
      type = types.int;
      default = 10;
      description = ''
        Limits for timeline cleanup.
      '';
    };

    TIMELINE_LIMIT_DAILY = mkOption {
      type = types.int;
      default = 10;
      description = ''
        Limits for timeline cleanup.
      '';
    };

    TIMELINE_LIMIT_WEEKLY = mkOption {
      type = types.int;
      default = 0;
      description = ''
        Limits for timeline cleanup.
      '';
    };

    TIMELINE_LIMIT_MONTHLY = mkOption {
      type = types.int;
      default = 10;
      description = ''
        Limits for timeline cleanup.
      '';
    };

    TIMELINE_LIMIT_QUARTERLY = mkOption {
      type = types.int;
      default = 0;
      description = ''
        Limits for timeline cleanup.
      '';
    };

    TIMELINE_LIMIT_YEARLY = mkOption {
      type = types.int;
      default = 10;
      description = ''
        Limits for timeline cleanup.
      '';
    };
  };
in

{
  options.services.snapper = {

    snapshotRootOnBoot = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Whether to snapshot root on boot
      '';
    };

    snapshotInterval = mkOption {
      type = types.str;
      default = "hourly";
      description = ''
        Snapshot interval.

        The format is described in
        {manpage}`systemd.time(7)`.
      '';
    };

    persistentTimer = mkOption {
      default = false;
      type = types.bool;
      example = true;
      description = ''
        Set the `Persistent` option for the
        {manpage}`systemd.timer(5)`
        which triggers the snapshot immediately if the last trigger
        was missed (e.g. if the system was powered down).
      '';
    };

    cleanupInterval = mkOption {
      type = types.str;
      default = "1d";
      description = ''
        Cleanup interval.

        The format is described in
        {manpage}`systemd.time(7)`.
      '';
    };

    filters = mkOption {
      type = types.nullOr types.lines;
      default = null;
      description = ''
        Global display difference filter. See man:snapper(8) for more details.
      '';
    };

    configs = mkOption {
      default = { };
      example = literalExpression ''
        {
          home = {
            SUBVOLUME = "/home";
            ALLOW_USERS = [ "alice" ];
            TIMELINE_CREATE = true;
            TIMELINE_CLEANUP = true;
          };
        }
      '';

      description = ''
        Subvolume configuration. Any option mentioned in man:snapper-configs(5)
        is valid here, even if NixOS doesn't document it.
      '';

      type = types.attrsOf (
        types.submodule {
          freeformType = types.attrsOf (
            types.oneOf [
              (types.listOf safeStr)
              types.bool
              safeStr
              types.number
            ]
          );

          options = configOptions;
        }
      );
    };
  };

  config = mkIf (cfg.configs != { }) (
    let
      documentation = [
        "man:snapper(8)"
        "man:snapper-configs(5)"
      ];
    in
    {
      environment = {

        systemPackages = [ pkgs.snapper ];

        # Note: snapper/config-templates/default is only needed for create-config
        #       which is not the NixOS way to configure.
        etc =
          {

            "sysconfig/snapper".text = ''
              SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
            '';
          }
          // (mapAttrs' (
            name: subvolume:
            nameValuePair "snapper/configs/${name}" ({
              text = lib.generators.toKeyValue { inherit mkKeyValue; } (
                filterAttrs (k: v: v != defaultOf k) subvolume
              );
            })
          ) cfg.configs)
          // (lib.optionalAttrs (cfg.filters != null) { "snapper/filters/default.txt".text = cfg.filters; });
      };

      services.dbus.packages = [ pkgs.snapper ];

      systemd.services.snapperd = {
        description = "DBus interface for snapper";
        inherit documentation;
        serviceConfig = {
          Type = "dbus";
          BusName = "org.opensuse.Snapper";
          ExecStart = "${pkgs.snapper}/bin/snapperd";
          CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE";
          LockPersonality = true;
          NoNewPrivileges = false;
          PrivateNetwork = true;
          ProtectHostname = true;
          RestrictAddressFamilies = "AF_UNIX";
          RestrictRealtime = true;
        };
      };

      systemd.services.snapper-timeline = {
        description = "Timeline of Snapper Snapshots";
        inherit documentation;
        requires = [ "local-fs.target" ];
        serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
      };

      systemd.timers.snapper-timeline = {
        wantedBy = [ "timers.target" ];
        timerConfig = {
          Persistent = cfg.persistentTimer;
          OnCalendar = cfg.snapshotInterval;
        };
      };

      systemd.services.snapper-cleanup = {
        description = "Cleanup of Snapper Snapshots";
        inherit documentation;
        serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
      };

      systemd.timers.snapper-cleanup = {
        description = "Cleanup of Snapper Snapshots";
        inherit documentation;
        wantedBy = [ "timers.target" ];
        requires = [ "local-fs.target" ];
        timerConfig.OnBootSec = "10m";
        timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
      };

      systemd.services.snapper-boot = lib.mkIf cfg.snapshotRootOnBoot {
        description = "Take snapper snapshot of root on boot";
        inherit documentation;
        serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
        serviceConfig.Type = "oneshot";
        requires = [ "local-fs.target" ];
        wantedBy = [ "multi-user.target" ];
        unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
      };

      assertions = concatMap (
        name:
        let
          sub = cfg.configs.${name};
        in
        [
          {
            assertion = !(sub ? extraConfig);
            message = ''
              The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it.
              The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'.
            '';
          }
        ]
        ++
          map
            (attr: {
              assertion = !(hasAttr attr sub);
              message = ''
                The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${toUpper attr}'.
              '';
            })
            [
              "fstype"
              "subvolume"
            ]
      ) (attrNames cfg.configs);
    }
  );

  meta.maintainers = with lib.maintainers; [ Djabx ];
}
