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

let
  cfg = config.programs.sessiond;

  sessionPackages = lib.mapAttrsToList
    (name: session:
      let
        displaySession = ''
          [Desktop Entry]
          Name=${session.fullName}
          Comment=${session.description}
          Exec="@out@/libexec/${name}-session"
          Type=Application
          DesktopNames=${lib.concatStringsSep ";" session.desktopNames};
        '';

        sessionScript = ''
          #!${pkgs.runtimeShell}

          ${lib.getExe' cfg.package "sessionctl"} run "${name}.target"
        '';
      in
      pkgs.runCommandLocal "${name}-desktop-session-files"
        {
          inherit displaySession sessionScript;
          passAsFile = [ "displaySession" "sessionScript" ];
          passthru.providedSessions = [ name ];
        }
        ''
          SESSION_SCRIPT="$out/libexec/${name}-session"
          install -Dm0755 "$sessionScriptPath" "$SESSION_SCRIPT"
          substituteAllInPlace "$SESSION_SCRIPT"

          DISPLAY_SESSION_FILE="$out/share/xsessions/${name}.desktop"
          install -Dm0644 "$displaySessionPath" "$DISPLAY_SESSION_FILE"
          substituteAllInPlace "$DISPLAY_SESSION_FILE"
        ''
    )
    cfg.sessions;

  sessionSystemdUnits = lib.mapAttrsToList
    (name: session:
      let
        inherit (utils.systemdUtils.lib)
          pathToUnit serviceToUnit targetToUnit timerToUnit socketToUnit;

        sessionComponents = 
          lib.foldlAttrs
            (acc: name: component:
              acc // {
                "${component.id}.service" = serviceToUnit component.id component.serviceUnit;
                "${component.id}.target" = targetToUnit component.id component.targetUnit;
              } // lib.optionalAttrs (component.socketUnit != null) {
                "${component.id}.socket" = socketToUnit component.id component.socketUnit;
              } // lib.optionalAttrs (component.timerUnit != null) {
                "${component.id}.timer" = timerToUnit component.id component.timerUnit;
              } // lib.optionalAttrs (component.pathUnit != null) {
                "${component.id}.path" = pathToUnit component.id component.pathUnit;
              })
            { }
            session.components;
      in
        sessionComponents // {
          "${name}.service" = serviceToUnit name session.serviceUnit;
          "${name}.target" = targetToUnit name session.targetUnit;
        }
    )
    cfg.sessions;
in
{
  options.programs.sessiond = {
    package = lib.mkOption {
      type = lib.types.package;
      default = pkgs.sessiond;
      defaultText = "pkgs.sessiond";
      description = ''
        The package containing sessiond executable and systemd units. This
        module will use the `sessiond` executable for the generated session
        script.
      '';
    };

    sessions = lib.mkOption {
      type = with lib.types; attrsOf (submoduleWith {
        specialArgs = {
          inherit utils pkgs;
          sessiondPkg = cfg.package;
        };
        modules = [ ./submodules/session-type.nix ];
      });
      example = lib.literalExpression ''
        {
          "com.example.Beepeedobolyuessemm" = {
            description = "Simple desktop environment featuring bspwm";
            desktopNames = [ "Beepeedobolyuessemm" ];

            settings = {
              Idle = {
                Inputs = [ "motion" "button-press" "key-press" ];
                IdleSec = 120;
              };

              Lock = {
                OnIdle = true;
                OnSleep = true;
                MuteAudio = true;
              };

              Hook = [
                {
                  Trigger = "Idle";
                  ExecStart = "''${lib.getExe' pkgs.betterlockscreen "betterlockscreen"} --off 240";
                }
              ];
            };

            components = {
              window-manager = {
                description = "Window manager";

                serviceUnit = {
                  # This is required for sessiond to recognize which unit is the
                  # window manager.
                  aliases = [ "window-manager.service" ];

                  script = '''
                    ''${lib.getExe' pkgs.bspwm "bspwm"} -c ''${./config/bspwm/bspwmrc}
                  ''';

                  serviceConfig = {
                    ExecStopPost = "''${lib.getExe' sessiondPkg "sessionctl"} stop";
                    OOMScoreAdjust = -1000;
                  };
                };

                targetUnit = {
                  requires = [ "sessiond-session.target" ];
                  partOf = [ "sessiond-session.target" ];
                  wantedBy = [ "sessiond-session.target" ];
                };
              };

              hotkey-daemon = {
                description = "Hotkey daemon";

                serviceUnit = {
                  documentation = [ "man:sxhkd(1)" ];

                  script = '''
                    ''${lib.getExe' pkgs.sxhkd "sxhkd"} -c ''${./config/sxhkd/bindings}
                  ''';

                  serviceConfig = {
                    ExecReload = "''${lib.getExe' pkgs.coreutils "kill"} -SIGUSR1 $MAINPID";
                    ExecStopPost = "''${lib.getExe' sessiondPkg "sessionctl"} stop";
                    OOMScoreAdjust = -1000;
                  };
                };

                targetUnit = {
                  after = [ "display-manager.service" ];
                  partOf = [ "sessiond-session.target" ];
                };
              };
            };
          };
        }
      '';
      description = ''
        A set of desktop sessions to be configured with sessiond. Each of the
        attribute name will be used as the identifier of the desktop
        environment.

        ::: {.tip}
        While you can make identifiers in any way, it is
        encouraged to stick to a naming scheme. Here's two common ways to name
        a desktop environment.

        * Reverse DNS-like scheme (e.g., `com.example.MoseyBranch`).
        * Kebab-case (e.g., `mosey-branch`).
        :::
      '';
      default = { };
    };
  };

  config = lib.mkIf (cfg.sessions != { }) {
    environment.systemPackages = [ cfg.package ];

    # Install all of the desktop session files.
    services.xserver.displayManager.sessionPackages = sessionPackages;

    # Import those systemd units from sessiond as well.
    systemd.packages = [ cfg.package ];
    systemd.user.units = lib.mkMerge sessionSystemdUnits;

    # We're disabling the upstream sessiond service since we have our own set
    # of sessiond sessions here.
    systemd.user.services.sessiond.enable = false;
  };
}