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

with utils;
with lib;

let
  cfg = config.services.borgbackup;

  lvmPath = {
    options = {
      LV = mkOption {
        type = types.str;
      };
      VG = mkOption {
        type = types.str;
      };
    };
  };

  pathType = if cfg.snapshots == "lvm" then types.submodule lvmPath else types.path;

  systemdPath = path: escapeSystemdPath (if cfg.snapshots == "lvm" then "${path.VG}-${path.LV}" else path);

  withSuffix = path: path + (if cfg.snapshots == "btrfs" then config.services.btrfs-snapshots.mountSuffix else config.services.lvm-snapshots.mountSuffix);

  mountPoint = if cfg.snapshots == "lvm" then config.services.lvm-snapshots.mountPoint else "";

  targetOptions = {
    options = {
      repo = mkOption {
        type = types.str;
      };
    
      paths = mkOption {
        type = types.listOf pathType;
        default = [];
      };

      prune = mkOption {
        type = types.attrsOf (types.listOf types.str);
        default = {};
      };

      interval = mkOption {
        type = types.str;
        default = "6h";
      };

      jitter = mkOption {
        type = with types; nullOr str;
        default = "6h";
      };

      lock = mkOption {
        type = types.nullOr types.str;
        default = "backup";
      };

      network = mkOption {
        type = types.bool;
        default = true;
      };

      lockWait = mkOption {
        type = types.int;
        default = 600;
      };

      keyFile = mkOption {
        type = types.nullOr types.path;
        default = null;
      };
    };
  };
in {
  disabledModules = [ "services/backup/borgbackup.nix" ];
  
  options = {
    services.borgbackup = {
      snapshots = mkOption {
        type = types.nullOr (types.enum ["btrfs" "lvm"]);
        default = null;
      };

      targets = mkOption {
        type = types.attrsOf (types.submodule targetOptions);
        default = {};
      };

      prefix = mkOption {
        type = types.str;
      };
    };
  };

  imports =
    [ ./lvm-snapshots.nix
      ./btrfs-snapshots.nix
    ];

  config = mkIf (any (t: t.paths != []) (attrValues cfg.targets)) {
    services.btrfs-snapshots.enable = mkIf (cfg.snapshots == "btrfs") true;

    services.lvm-snapshots.snapshots = mkIf (cfg.snapshots == "lvm") (listToAttrs (map (path: nameValuePair (path.VG + "-" + path.LV) {
      inherit (path) LV VG; 
      mountName = withSuffix (path.VG + "-" + path.LV);
    }) (unique (flatten (mapAttrsToList (target: tCfg: tCfg.paths) cfg.targets)))));

    systemd.targets."timers-borg" = {
      wantedBy = [ "timers.target" ];
    };

    systemd.slices."system-borgbackup" = {};
  
    systemd.timers = (listToAttrs (map ({ target, path, tCfg }: nameValuePair "borgbackup-${target}@${systemdPath path}" {
      requiredBy = [ "timers-borg.target" ];

      timerConfig = {
        Persistent = false;
        OnBootSec = tCfg.interval;
        OnUnitActiveSec = tCfg.interval;
        RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
      };
    }) (flatten (mapAttrsToList (target: tCfg: map (path: { inherit target path tCfg; }) tCfg.paths) cfg.targets)))) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
      enable = tCfg.prune != {};
    
      requiredBy = [ "timers-borg.target" ];

      timerConfig = {
        Persistent = false;
        OnBootSec = tCfg.interval;
        OnUnitActiveSec = tCfg.interval;
        RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
      };
    }) cfg.targets);

    systemd.services = (mapAttrs' (target: tCfg: nameValuePair "borgbackup-${target}@" (let
      deps = flatten [
        (optional (cfg.snapshots == "btrfs") "btrfs-snapshot@%i.service")
        (optional tCfg.network "network-online.target")
      ];
    in {
      bindsTo = deps;
      after = deps;

      path = with pkgs; [borgbackup] ++ optional (tCfg.lock != null) utillinux;

      script = let
        borgCmd = ''
          borg create \
            --lock-wait ${toString tCfg.lockWait} \
            --stats \
            --list \
            --filter 'AME' \
            --exclude-caches \
            --keep-exclude-tags \
            --patterns-from .backup-${target} \
            --one-file-system \
            --compression auto,lzma \
            ${tCfg.repo}::${cfg.prefix}$1-{utcnow}
        '';
      in if tCfg.lock == null then borgCmd else "flock -xo /var/lock/${tCfg.lock} ${borgCmd}";
      scriptArgs = if cfg.snapshots == "lvm" then "%I" else "%i";

      unitConfig = {
        AssertPathIsDirectory = mkIf (tCfg.lock != null) "/var/lock";
        DefaultDependencies = false;
        RequiresMountsFor = mkIf (cfg.snapshots == "lvm") [ "${mountPoint}/${withSuffix "%I"}" ];
      };

      serviceConfig = {
        Type = "oneshot";
        WorkingDirectory = if (cfg.snapshots == null) then "%I" else (if (cfg.snapshots == "lvm") then "${mountPoint}/${withSuffix "%I"}" else "${withSuffix "%f"}");
        Nice = 15;
        IOSchedulingClass = 2;
        IOSchedulingPriority = 7;
        SuccessExitStatus = [1 2];
        Slice = "system-borgbackup.slice";
        Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}";
      };
    })) cfg.targets) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
      enable = tCfg.prune != {};
    
      bindsTo = ["network-online.target"];
      after = ["network-online.target"];

      path = with pkgs; [borgbackup];

      script = concatStringsSep "\n" (mapAttrsToList (path: args: ''
        borg prune \
          --lock-wait ${toString tCfg.lockWait} \
          --list \
          --stats \
          --prefix ${escapeShellArg "${cfg.prefix}${path}"} \
          ${escapeShellArgs args} \
          ${tCfg.repo}
      '') tCfg.prune);

      serviceConfig = {
        Type = "oneshot";
        Slice = "system-borgbackup.slice";
        Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}";
      };
    }) cfg.targets);
  };
}