{ inputs, lib, pkgs, config, ... }:
let
  inherit (builtins) substring match;
  inherit (lib)
    nameValuePair optional optionalString optionalAttrs mapAttrs' mapAttrsToList concatStringsSep
    concatMapStringsSep mkIf;
  inherit (lib.my) mkOpt' mkBoolOpt';

  jobType = with lib.types; submodule ({ name, ... }@args:
  let
    cfg = args.config;
  in
  {
    options = {
      repo = mkOpt' str null "borg repository URL";
      passFile = mkOpt' (nullOr str) null "Path to file containing passphrase";

      archivePrefix = mkOpt' str "${config.networking.hostName}-${name}-" "Prefix to start new archives with";
      dateFormat = mkOpt' str "+%Y-%m-%dT%H:%M:%S" "Format passed to the date command";
      compression = mkOpt' str "zstd,3" "Compression options";
      lvs = mkOpt' (listOf str) null "Thin LVs to backup (vg/lv format)";
      prune = {
        pattern = mkOpt' str "sh:${cfg.archivePrefix}*" "Borg pattern to select archives for pruning";
        keep = mkOpt' (attrsOf (either int str)) { } "Borg pruning params";
      };

      extraArgs = mkOpt' (listOf str) [ "--iec" ] "Extra args to pass to all borg commands";
      extraCreateArgs = mkOpt' (listOf str) [ ] "Extra args to pass to tcreate command";
      environment = mkOpt' (attrsOf str) { } "Extra environment variables to pass to borg";

      timer = {
        at = mkOpt' (either str (listOf str)) "5:00" "systemd calendar time(s) to run backup at";
        persistent = mkBoolOpt' false "Persistent systemd timer";
      };
    };
  });

  cfg' = config.my.borgthin;

  isLocalPath = x:
    substring 0 1 x == "/"      # absolute path
    || substring 0 1 x == "."   # relative path
    || match "[.*:.*]" == null; # not machine:path

  argStr = concatMapStringsSep " " (a: ''"${a}"'');

  mkEnv = name: cfg: (rec {
    BORG_BASE_DIR = "/var/lib/borgthin";
    BORG_CONFIG_DIR = "${BORG_BASE_DIR}/config";
    BORG_CACHE_DIR = "/var/cache/borgthin";

    BORG_REPO = cfg.repo;
  }) //
  (optionalAttrs (cfg.passFile != null) {
    BORG_PASSCOMMAND = "cat ${cfg.passFile}";
  }) //
  cfg.environment;

  # utility function around makeWrapper
  mkWrapperDrv = {
    original, name,
    set ? { }, addFlags ? [ ],
  }:
  pkgs.runCommand "${name}-wrapper" {
    nativeBuildInputs = [ pkgs.makeWrapper ];
  } ''
    makeWrapper "${original}" "$out/bin/${name}" \
      ${concatStringsSep " \\\n " (
        (mapAttrsToList (name: value: ''--set ${name} "${value}"'') set) ++
        (map (f: ''--add-flags "${f}"'') addFlags)
      )}
  '';
  mkBorgWrapper = name: cfg: mkWrapperDrv {
    original = "${cfg'.package}/bin/borgthin";
    name = "borgthin-job-${name}";
    set = mkEnv name cfg;
    addFlags = cfg.extraArgs;
  };

  mkKeepArgs = keep:
    # If cfg.prune.keep e.g. has a yearly attribute,
    # its content is passed on as --keep-yearly
    concatStringsSep " "
      (mapAttrsToList (x: y: "--keep-${x}=${toString y}") keep);

  mkService = name: cfg: nameValuePair "borgthin-job-${name}" {
    description = "borgthin backup job ${name}";
    serviceConfig = {
      Type = "oneshot";
      StateDirectory = "borgthin";
      CacheDirectory = "borgthin";

      # Only run when no other process is using CPU or disk
      CPUSchedulingPolicy = "idle";
      IOSchedulingClass = "idle";
    };
    environment = mkEnv name cfg;

    path = [ cfg'.lvmPackage cfg'.thinToolsPackage cfg'.package ];
    script = ''
      extraArgs="${argStr cfg.extraArgs}"
      borgthin $extraArgs tcreate \
        --compression "${cfg.compression}" \
        ${argStr cfg.extraCreateArgs} \
        "${cfg.archivePrefix}$(date "${cfg.dateFormat}")" \
        ${concatStringsSep " " cfg.lvs}
    '' + optionalString (cfg.prune.keep != { }) ''
      borgthin $extraArgs prune \
        --match-archives "${cfg.prune.pattern}" \
        ${mkKeepArgs cfg.prune.keep}
      borgthin $extraArgs compact
    '';
  };

  mkTimer = name: cfg: nameValuePair "borgthin-job-${name}" {
    description = "borgthin backup job ${name} timer";
    after = optional (cfg.timer.persistent && !isLocalPath cfg.repo) "network-online.target";
    timerConfig = {
      Persistent = cfg.timer.persistent;
      OnCalendar = cfg.timer.at;
    };
    wantedBy = [ "timers.target" ];
  };
in
{
  options.my.borgthin = with lib.types; {
    enable = mkBoolOpt' false "Whether to enable borgthin jobs";
    lvmPackage = mkOpt' package pkgs.lvm2 "Packge containing LVM tools";
    thinToolsPackage = mkOpt' package pkgs.thin-provisioning-tools "Package containing thin-provisioning-tools";
    # Really we should use the version from the overlay, but the package is quite far behind...
    # Not bothering to update until Borg 2.0 releases
    package = mkOpt' package inputs.borgthin.packages.${config.nixpkgs.system}.borgthin "borgthin package";
    jobs = mkOpt' (attrsOf jobType) { } "borgthin jobs";
  };

  config = mkIf cfg'.enable {
    environment.systemPackages =
      [ cfg'.package ] ++
      (mapAttrsToList mkBorgWrapper cfg'.jobs);

    systemd = {
      services = mapAttrs' mkService cfg'.jobs;
      timers = mapAttrs' mkTimer cfg'.jobs;
    };

    my.tmproot.persistence.config.directories = [
      "/var/lib/borgthin"
      "/var/cache/borgthin"
    ];
  };
}

