{ config, lib, pkgs, sane-lib, ... }:
let
  toUnitName = s:
    # TODO: i could query `services."${s}".command` to figure out here if it's
    # a target or a service, rather than hardcode it.
    if lib.elem s [ "default" "graphical-session" "gps" "private-storage" "sound" "wayland" "x11" ] then
      "${s}.target"
    else
      "${s}.service"
  ;
  toUnitNames = lib.map toUnitName;
  mkSystemdUnit = {
    description,
    documentation,
    depends,
    dependencyOf,
    partOf,
    ...
  }: {
    inherit description documentation;
    wants = toUnitNames depends;
    after = toUnitNames depends;
    wantedBy = (toUnitNames dependencyOf) ++ (toUnitNames partOf);
    before = (toUnitNames dependencyOf) ++ (toUnitNames partOf);
  };
  mkSystemdService = userName: userConfig: _serviceName: {
    # description,
    # documentation,
    # depends,
    # dependencyOf,
    # partOf,
    command,
    cleanupCommand,
    startCommand,
    readiness,  # readiness.waitCommand, readiness.waitDbus, readiness.waitExists
    reapChildren,
    restartCondition,
    ...
  }: lib.mkMerge [
    {
      # serviceConfig.BoundBy = toUnitNames partOf;
      # serviceConfig.Restart = restartCondition;  #< not allowed for `oneshot` types

      serviceConfig.KillMode = if reapChildren then
        "control-group"  # (default)
      else
        "process"  # kill only the main process; let it leave behind children if it likes
      ;

      serviceConfig.User = userName;
      serviceConfig.Group = "users";
      serviceConfig.WorkingDirectory = "~";
      serviceConfig.X-RestartIfChanged = lib.mkDefault false;  #< NixOS attribute, so we don't restart the DE on activation

      serviceConfig.ExecSearchPath = [
        "/etc/profiles/per-user/${userName}/bin"
        "/run/current-system/sw/bin"
      ];
      path = [
        "/etc/profiles/per-user/${userName}"
        "/run/current-system/sw"
      ];
      # systemd doesn't allow substitutions, so perform one layer of substitution over the environment.
      # especially, this lets me base environment variables off of XDG dirs (like XDG_RUNTIME_DIR).
      environment = lib.mapAttrs
        (k: v: lib.replaceStrings
          (lib.mapAttrsToList (var: value: "$" + var) userConfig.environment)
          (lib.mapAttrsToList (var: value: value) userConfig.environment)
          v
        )
        userConfig.environment;
    }
    (lib.mkIf (command != null && readiness.waitCommand == null) {
      serviceConfig.Type = "simple";
      serviceConfig.Restart = restartCondition;
      startLimitIntervalSec = 0;  #< disable restart limit
      serviceConfig.ExecStart = command;
    })
    (lib.mkIf (command != null && readiness.waitCommand != null) {
      serviceConfig.Type = "notify";
      serviceConfig.Restart = restartCondition;
      startLimitIntervalSec = 0;  #< disable restart limit
      # serviceConfig.NotifyAccess = "exec";  #< allow anything in Exec* to invoke systemd-notify
      serviceConfig.NotifyAccess = "all";  #< allow anything in Exec* to invoke systemd-notify
      script = ''
        isReady() {
          echo "checking readiness..."
          ${readiness.waitCommand}
        }
        readinessPollLoop() {
          while ! isReady; do
            echo "service is not ready: sleeping 1s"
            ${lib.getExe' pkgs.coreutils "sleep"} 1
          done
          echo "ready: notifying systemd"
          ${lib.getExe' pkgs.systemd "systemd-notify"} --ready
        }
        cleanup() {
          # kill all direct children
          # this is distinct from killing the whole process group,
          # which i explicitly *don't* do, to allow spawned processes to outlive the service
          #   (if reapChildren = false; else systemd implicitly reaps all the children)
          ${lib.getExe' pkgs.procps "pkill"} -P $$
        }

        trap cleanup EXIT

        readinessPollLoop &
        ${command}
        exit $?
      '';
    })
    (lib.mkIf (startCommand != null) {
      serviceConfig.Type = "oneshot";
      serviceConfig.RemainAfterExit = true;
      serviceConfig.ExecStart = startCommand;
    })
    (lib.mkIf (cleanupCommand != null) {
      serviceConfig.ExecStopPost = cleanupCommand;
      serviceConfig.TimeoutStopSec = "30s";  #< system-wide default is closer to 10s, but eg25-control-powered takes longer than that
    })
  ];

  mkSystemd = enable: userName: userConfig: unitName: unitConfig: let
    unit = mkSystemdUnit unitConfig;
    service = mkSystemdService userName userConfig unitName unitConfig;
    isService = unitConfig.command != null || unitConfig.startCommand != null;
    # isService = (service.serviceConfig.Type or null) != null;
  in {
    # XXX: can't use `systemd.units ... = unit` because it doesn't expose `after`
    systemd.services = lib.optionalAttrs (enable && isService) {
      "${unitName}" = lib.mkMerge [ unit service ];
    };
    # systemd complains if you manually define `default.target`, so omit that particular one
    systemd.targets = lib.optionalAttrs (enable && !isService && unitName != "default") {
      "${unitName}" = unit;
    };
  };

  configsForUser = userName: userConfig: let
    globalEn = userConfig.default && userConfig.serviceManager == "systemd";
  in
    lib.mapAttrsToList (mkSystemd globalEn userName userConfig) userConfig.services
  ;
in
{
  config = let
    configs = lib.flatten (lib.mapAttrsToList configsForUser config.sane.users);
    take = f: {
      systemd.services = f.systemd.services;
      systemd.targets = f.systemd.targets;
    };
  in lib.mkMerge [
    (take (sane-lib.mkTypedMerge take configs))
  ];
  # systemd.services = lib.mkMerge (lib.mapAttrsToList servicesForUser config.sane.users);
}
