{
  config,
  lib,
  pkgs,
  ...
}: let
  cfg = config.services.nix-sync;

  mkTimer = name: repo: {
    description = "Nix sync ${name} timer";
    wantedBy = ["timers.target"];
    timerConfig = {
      OnActiveSec = repo.interval;
    };
    after = ["network-online.target"];
  };

  parents = path: let
    split_path = builtins.split "/" path;
    filename = builtins.elemAt split_path (builtins.length split_path - 1);
  in
    lib.strings.removeSuffix "/" (builtins.replaceStrings [filename] [""] path);
  esa = lib.strings.escapeShellArg;
  mkUnit = name: repo: let
    optionalPathSeparator =
      if lib.strings.hasPrefix "/" repo.path
      then ""
      else "/";
    repoCachePath = cfg.cachePath + optionalPathSeparator + repo.path;
    execStartScript = pkgs.writeScript "nix-sync-exec" ''
      #! /usr/bin/env dash
      export XDG_CACHE_HOME="$CACHE_DIRECTORY";
      cd ${esa repoCachePath};

      git fetch
      origin="$(git rev-parse @{u})";
      branch="$(git rev-parse @)";

      if ! [ "$origin" = "$branch" ]; then
        git pull;

        out_paths=$(mktemp);
        nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
        [ "$(wc -l < "$out_paths")" -gt 1 ] && (echo "To many out-paths"; exit 1)
        out_path="$(cat "$out_paths")";
        rm ${esa repo.path};
        ln -s "$out_path" ${esa repo.path};
        rm "$out_paths";
      fi
    '';
    execStartPreScript = ''
      export XDG_CACHE_HOME="$CACHE_DIRECTORY";

      if ! [ -d ${esa repoCachePath}/.git ]; then
          mkdir --parents ${esa repoCachePath};
          git clone ${esa repo.uri} ${esa repoCachePath};

          out_paths=$(mktemp);
          nix build ${esa repoCachePath} --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
          [ "$(wc -l < "$out_paths")" -gt 1 ] && (echo "To many out-paths"; exit 1)
          out_path="$(cat "$out_paths")";
          ln -s "$out_path" ${esa repo.path};
          rm "$out_paths";
      fi

      if ! [ -L ${esa repo.path} ]; then
        cd ${esa repoCachePath};

        git pull;

        out_paths=$(mktemp);
        nix build . --print-out-paths --experimental-features 'nix-command flakes' > "$out_paths";
        [ "$(wc -l < "$out_paths")" -gt 1 ] && (echo "To many out-paths"; exit 1)
        out_path="$(cat "$out_paths")";

        [ -d ${esa repo.path} ] && rm -d ${esa repo.path};
        [ -e ${esa repo.path} ] && rm ${esa repo.path};

        ln -s "$out_path" ${esa repo.path};
        rm "$out_paths";
      fi
    '';
  in {
    description = "Nix Sync ${name}";
    wantedBy = ["default.target"];
    after = ["network.target"];
    path = with pkgs; [openssh git nix mktemp coreutils dash];
    preStart = execStartPreScript;

    serviceConfig = {
      ExecStart = execStartScript;
      Restart = "on-abort";
      # User and group
      User = cfg.user;
      Group = cfg.group;
      # Runtime directory and mode
      RuntimeDirectory = "nix-sync";
      RuntimeDirectoryMode = "0750";
      # Cache directory and mode
      CacheDirectory = "nix-sync";
      CacheDirectoryMode = "0750";
      # Logs directory and mode
      LogsDirectory = "nix-sync";
      LogsDirectoryMode = "0750";
      # Proc filesystem
      ProcSubset = "pid";
      ProtectProc = "invisible";
      # New file permissions
      UMask = "0027"; # 0640 / 0750
      # Capabilities
      AmbientCapabilities = ["CAP_CHOWN"];
      CapabilityBoundingSet = ["CAP_CHOWN"];
      # Security
      NoNewPrivileges = true;
      # Sandboxing (sorted by occurrence in https://www.freedesktop.org/software/systemd/man/systemd.exec.html)
      ReadWritePaths = ["${esa (parents repo.path)}" "-${esa repoCachePath}" "-${esa cfg.cachePath}"];
      ReadOnlyPaths = ["/nix"];
      ProtectSystem = "strict";
      ProtectHome = true;
      PrivateTmp = true;
      PrivateDevices = true;
      ProtectHostname = true;
      ProtectClock = true;
      ProtectKernelTunables = true;
      ProtectKernelModules = true;
      ProtectKernelLogs = true;
      ProtectControlGroups = true;
      RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
      RestrictNamespaces = true;
      LockPersonality = true;
      MemoryDenyWriteExecute = true;
      RestrictRealtime = true;
      RestrictSUIDSGID = true;
      RemoveIPC = true;
      PrivateMounts = true;
      # System Call Filtering
      SystemCallArchitectures = "native";
      SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"];
    };
  };

  services =
    lib.mapAttrs' (name: repo: {
      name = "nix-sync-${name}";
      value = mkUnit name repo;
    })
    cfg.repositories;
  timers =
    lib.mapAttrs' (name: repo: {
      name = "nix-sync-${name}";
      value = mkTimer name repo;
    })
    cfg.repositories;

  # generate the websites directory, so systemd can mount it read write
  generatedDirectories =
    lib.mapAttrsToList (
      _: repo: "d ${esa (parents repo.path)} 0755 ${cfg.user} ${cfg.group}"
    )
    cfg.repositories;

  repositoryType = lib.types.submodule ({name, ...}: {
    options = {
      name = lib.mkOption {
        internal = true;
        default = name;
        type = lib.types.str;
        description = "The name that should be given to this unit.";
      };

      path = lib.mkOption {
        type = lib.types.str;
        description = "The path at which to sync the repository";
      };

      uri = lib.mkOption {
        type = lib.types.str;
        example = "ssh://user@example.com:/~[user]/path/to/repo.git";
        description = ''
          The URI of the remote to be synchronized. This is only used in the
          event that the directory does not already exist. See
          <link xlink:href="https://git-scm.com/docs/git-clone#_git_urls"/>
          for the supported URIs.
        '';
      };

      interval = lib.mkOption {
        type = lib.types.int;
        default = 500;
        description = ''
          The interval, specified in seconds, at which the synchronization will
          be triggered.
        '';
      };
    };
  });
in {
  options = {
    services.nix-sync = {
      enable = lib.mkEnableOption "nix-sync services";

      user = lib.mkOption {
        type = lib.types.str;
        default = "nix-sync";
        description = lib.mdDoc "User account under which nix-sync units runs.";
      };

      group = lib.mkOption {
        type = lib.types.str;
        default = "nix-sync";
        description = lib.mdDoc "Group account under which nix-sync units runs.";
      };

      cachePath = lib.mkOption {
        type = lib.types.str;
        default = "/var/lib/nix-sync";
        description = lib.mdDoc ''
          Where to cache git directories. Should not end with a slash ("/")
        '';
      };

      repositories = lib.mkOption {
        type = with lib.types; attrsOf repositoryType;
        description = ''
          The repositories that should be synchronized.
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      {
        assertion = !lib.strings.hasSuffix "/" cfg.cachePath;
        message = "Your cachePath ('${cfg.cachePath}') ends with a slash ('/'), please use: '${lib.strings.removeSuffix "/" cfg.cachePath}'.";
      }
    ];

    systemd.tmpfiles.rules =
      generatedDirectories;

    systemd.services = services;
    systemd.timers = timers;
    users.users =
      if cfg.user == "nix-sync"
      then {
        nix-sync = {
          group = "${cfg.group}";
          isSystemUser = true;
        };
      }
      else lib.warnIf (cfg.user != "nix-sync") "The user (${cfg.user}) is not \"nix-sync\", thus you are responible for generating it.";
    users.groups =
      if cfg.group == "nix-sync"
      then {
        nix-sync = {
          members = ["${cfg.user}"];
        };
      }
      else lib.warnIf (cfg.group != "nix-sync") "The group (${cfg.group}) is not \"nix-sync\", thus you are responible for generating it.";
  };
}
# vim: ts=2