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

let

  cfg = config.services.displayManager.gdm;
  gdm = pkgs.gdm;
  xdmcfg = config.services.xserver.displayManager;
  pamLogin = config.security.pam.services.login;
  settingsFormat = pkgs.formats.ini { };
  configFile = settingsFormat.generate "custom.conf" cfg.settings;

  xSessionWrapper =
    if (xdmcfg.setupCommands == "") then
      null
    else
      pkgs.writeScript "gdm-x-session-wrapper" ''
        #!${pkgs.bash}/bin/bash
        ${xdmcfg.setupCommands}
        exec "$@"
      '';

  # Solves problems like:
  # https://wiki.archlinux.org/index.php/Talk:Bluetooth_headset#GDMs_pulseaudio_instance_captures_bluetooth_headset
  # Instead of blacklisting plugins, we use Fedora's PulseAudio configuration for GDM:
  # https://src.fedoraproject.org/rpms/gdm/blob/master/f/default.pa-for-gdm
  pulseConfig = pkgs.writeText "default.pa" ''
    load-module module-device-restore
    load-module module-card-restore
    load-module module-udev-detect
    load-module module-native-protocol-unix
    load-module module-default-device-restore
    load-module module-always-sink
    load-module module-intended-roles
    load-module module-suspend-on-idle
    load-module module-position-event-sounds
  '';

  defaultSessionName = config.services.displayManager.defaultSession;

  setSessionScript = pkgs.callPackage ../x11/display-managers/account-service-util.nix { };

  greeterUsers = lib.genAttrs' [ null 1 2 3 4 ] (
    i:
    let
      # adding 1 to create `gdm-greeter{-2,-3,-4,-5}`
      suffix = lib.optionalString (i != null) "-${toString (i + 1)}";
    in
    lib.nameValuePair "gdm-greeter${suffix}" {
      isSystemUser = true;
      uid = 60578 + (if i == null then 0 else i);
      group = "gdm";
      home = "/run/gdm/home/gdm-greeter${suffix}";
    }
  );
in

{
  imports = [
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "autoLogin" "enable" ]
      [
        "services"
        "displayManager"
        "autoLogin"
        "enable"
      ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "autoLogin" "user" ]
      [
        "services"
        "displayManager"
        "autoLogin"
        "user"
      ]
    )

    (lib.mkRemovedOptionModule [
      "services"
      "xserver"
      "displayManager"
      "gdm"
      "nvidiaWayland"
    ] "We defer to GDM whether Wayland should be enabled.")

    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "enable" ]
      [ "services" "displayManager" "gdm" "enable" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "debug" ]
      [ "services" "displayManager" "gdm" "debug" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "banner" ]
      [ "services" "displayManager" "gdm" "banner" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "settings" ]
      [ "services" "displayManager" "gdm" "settings" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "wayland" ]
      [ "services" "displayManager" "gdm" "wayland" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "autoSuspend" ]
      [ "services" "displayManager" "gdm" "autoSuspend" ]
    )
    (lib.mkRenamedOptionModule
      [ "services" "xserver" "displayManager" "gdm" "autoLogin" "delay" ]
      [ "services" "displayManager" "gdm" "autoLogin" "delay" ]
    )
    (lib.mkRemovedOptionModule [
      "services"
      "displayManager"
      "gdm"
      "wayland"
    ] "Disabling this option is no longer supported with GNOME 50.")
  ];

  meta = {
    teams = [ lib.teams.gnome ];
  };

  ###### interface

  options = {

    services.displayManager.gdm = {

      enable = lib.mkEnableOption "GDM, the GNOME Display Manager";

      debug = lib.mkEnableOption "debugging messages in GDM";

      # Auto login options specific to GDM
      autoLogin.delay = lib.mkOption {
        type = lib.types.int;
        default = 0;
        description = ''
          Seconds of inactivity after which the autologin will be performed.
        '';
      };

      autoSuspend = lib.mkOption {
        default = true;
        description = ''
          On the GNOME Display Manager login screen, suspend the machine after inactivity.
          (Does not affect automatic suspend while logged in, or at lock screen.)
        '';
        type = lib.types.bool;
      };

      banner = lib.mkOption {
        type = lib.types.nullOr lib.types.lines;
        default = null;
        example = ''
          foo
          bar
          baz
        '';
        description = ''
          Optional message to display on the login screen.
        '';
      };

      settings = lib.mkOption {
        type = settingsFormat.type;
        default = { };
        example = {
          debug.enable = true;
        };
        description = ''
          Options passed to the gdm daemon.
          See [here](https://help.gnome.org/admin/gdm/stable/configuration.html.en#daemonconfig) for supported options.
        '';
      };

      extraPackages = lib.mkOption {
        type = lib.types.listOf lib.types.package;
        default = [ ];
        example = lib.literalExpression "[ pkgs.gnome-themes-extra ]";
        description = ''
          Additional packages to add to XDG_DATA_DIRS for GDM.
          The `/share` directory of each package will be added to the data path.
        '';
      };

    };

  };

  ###### implementation

  config = lib.mkIf cfg.enable {

    warnings = lib.optional config.services.pulseaudio.enable "Support for Pulseaudio + gdm will be removed in NixOS 26.11";

    services.xserver.displayManager.lightdm.enable = false;

    users.users = lib.mkMerge [
      {
        gdm = {
          name = "gdm";
          uid = config.ids.uids.gdm;
          group = "gdm";
          description = "GDM user";
        };
      }
      greeterUsers
    ];

    users.groups.gdm.gid = config.ids.gids.gdm;

    # GDM needs different xserverArgs, presumable because using wayland by default.
    services.xserver.display = null;
    services.xserver.verbose = null;

    services.displayManager = {
      # Enable desktop session data
      enable = true;

      generic = {
        enable = true;
        environment = {
          GDM_X_SERVER_EXTRA_ARGS = toString (lib.filter (arg: arg != "-terminate") xdmcfg.xserverArgs);
          XDG_DATA_DIRS = lib.makeSearchPath "share" (
            [
              gdm # for gnome-login.session
              config.services.displayManager.sessionData.desktops
              pkgs.gnome-control-center # for accessibility icon
              pkgs.adwaita-icon-theme
              pkgs.hicolor-icon-theme # empty icon theme as a base
            ]
            ++ cfg.extraPackages
          );
        }
        // lib.optionalAttrs (xSessionWrapper != null) {
          # Make GDM use this wrapper before running the session, which runs the
          # configured setupCommands. This relies on a patched GDM which supports
          # this environment variable.
          GDM_X_SESSION_WRAPPER = "${xSessionWrapper}";
        };
        execCmd = "exec ${gdm}/bin/gdm";
        preStart = lib.optionalString (defaultSessionName != null) ''
          # Set default session in session chooser to a specified values – basically ignore session history.
          ${setSessionScript}/bin/set-session ${config.services.displayManager.sessionData.autologinSession}
        '';
      };
    };

    systemd.tmpfiles.rules =
      lib.optionals config.services.pulseaudio.enable (
        lib.concatLists (
          lib.mapAttrsToList (name: user: [
            "d ${user.home}/.config 0711 ${name} gdm"
            "d ${user.home}/.config/pulse 0711 ${name} gdm"
            "L+ ${user.home}/.config/pulse/${pulseConfig.name} - - - - ${pulseConfig}"
          ]) greeterUsers
        )
      )
      ++ lib.optionals config.services.gnome.gnome-initial-setup.enable [
        # Create stamp file for gnome-initial-setup to prevent it starting in GDM.
        "f /run/gdm/gdm.ran-initial-setup 0711 gdm gdm - yes"
      ];

    # Otherwise GDM will not be able to start correctly and display Wayland sessions
    systemd.packages = [
      gdm
      pkgs.gnome-session
      pkgs.gnome-shell
    ];
    environment.systemPackages = [
      pkgs.adwaita-icon-theme
      pkgs.gdm # For polkit rules
    ];

    # We dont use the upstream gdm service
    # it has to be disabled since the gdm package has it
    # https://github.com/NixOS/nixpkgs/issues/108672
    systemd.services.gdm.enable = false;

    systemd.services.display-manager.wants = [
      # Because sd_login_monitor_new requires /run/systemd/machines
      "systemd-machined.service"
      # setSessionScript wants AccountsService
      "accounts-daemon.service"
    ];

    systemd.services.display-manager.after = [
      "rc-local.service"
      "systemd-machined.service"
      "systemd-user-sessions.service"
      "plymouth-quit.service"
      "plymouth-start.service"
    ];
    systemd.services.display-manager.conflicts = [
      "plymouth-quit.service"
    ];
    systemd.services.display-manager.onFailure = [
      "plymouth-quit.service"
    ];

    # Prevent nixos-rebuild switch from bringing down the graphical
    # session. (If multi-user.target wants plymouth-quit.service which
    # conflicts display-manager.service, then when nixos-rebuild
    # switch starts multi-user.target, display-manager.service is
    # stopped so plymouth-quit.service can be started.)
    systemd.services.plymouth-quit = lib.mkIf config.boot.plymouth.enable {
      wantedBy = lib.mkForce [ ];
    };

    systemd.services.display-manager.serviceConfig = {
      # Restart = "always"; - already defined in xserver.nix
      KillMode = "mixed";
      IgnoreSIGPIPE = "no";
      BusName = "org.gnome.DisplayManager";
      StandardError = "inherit";
      ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
      KeyringMode = "shared";
      EnvironmentFile = "-/etc/locale.conf";
    };

    systemd.services.display-manager.path = [ pkgs.gnome-session ];

    # Allow choosing an user account
    services.accounts-daemon.enable = true;

    services.dbus.packages = [ gdm ];

    systemd.user.services.dbus.wantedBy = [ "default.target" ];

    programs.dconf.profiles.gdm.databases =
      lib.optionals (!cfg.autoSuspend) [
        {
          settings."org/gnome/settings-daemon/plugins/power" = {
            sleep-inactive-ac-type = "nothing";
            sleep-inactive-battery-type = "nothing";
            sleep-inactive-ac-timeout = lib.gvariant.mkInt32 0;
            sleep-inactive-battery-timeout = lib.gvariant.mkInt32 0;
          };
        }
      ]
      ++ lib.optionals (cfg.banner != null) [
        {
          settings."org/gnome/login-screen" = {
            banner-message-enable = true;
            banner-message-text = cfg.banner;
          };
        }
      ]
      ++ [ "${gdm}/share/gdm/greeter-dconf-defaults" ];

    # Use AutomaticLogin if delay is zero, because it's immediate.
    # Otherwise with TimedLogin with zero seconds the prompt is still
    # presented and there's a little delay.
    services.displayManager.gdm.settings = {
      daemon = lib.mkMerge [
        # nested if else didn't work
        (lib.mkIf (config.services.displayManager.autoLogin.enable && cfg.autoLogin.delay != 0) {
          TimedLoginEnable = true;
          TimedLogin = config.services.displayManager.autoLogin.user;
          TimedLoginDelay = cfg.autoLogin.delay;
        })
        (lib.mkIf (config.services.displayManager.autoLogin.enable && cfg.autoLogin.delay == 0) {
          AutomaticLoginEnable = true;
          AutomaticLogin = config.services.displayManager.autoLogin.user;
        })
      ];
      debug = lib.mkIf cfg.debug {
        Enable = true;
      };
    };

    environment.etc."gdm/custom.conf".source = configFile;

    environment.etc."gdm/Xsession".source = config.services.displayManager.sessionData.wrapper;

    # GDM LFS PAM modules, adapted somehow to NixOS
    security.pam.services = {
      gdm-launch-environment = {
        useDefaultRules = false;
        rules = {
          auth = utils.pam.autoOrderRules [
            {
              name = "gdm-user";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
              settings.audit = true;
              settings.quiet_success = true;
              args = lib.mkAfter [
                "user"
                "ingroup"
                "gdm"
              ];
            }
            {
              name = "permit";
              control = "optional";
              modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
            }
          ];

          account = utils.pam.autoOrderRules [
            {
              name = "gdm-user";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
              settings.audit = true;
              settings.quiet_success = true;
              args = lib.mkAfter [
                "user"
                "ingroup"
                "gdm"
              ];
            }
            {
              name = "unix";
              control = "sufficient";
              modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
            }
          ];

          password = utils.pam.autoOrderRules [
            {
              name = "deny";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
            }
          ];

          session = utils.pam.autoOrderRules [
            {
              name = "gdm-user";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
              settings.audit = true;
              settings.quiet_success = true;
              args = lib.mkAfter [
                "user"
                "ingroup"
                "gdm"
              ];
            }
            {
              name = "env";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
              settings.conffile = "/etc/pam/environment";
              settings.readenv = 0;
            }
            # make sure the spawned session has the same variables as `display-manager.service`
            # https://github.com/NixOS/nixpkgs/issues/523332
            {
              name = "env-greeter";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
              settings.conffile =
                let
                  env = config.services.displayManager.generic.environment;
                in
                pkgs.writeText "gdm-launch-environment-env-conf" ''
                  PATH          DEFAULT="''${PATH}:${pkgs.gnome-session}/bin"
                  XDG_DATA_DIRS DEFAULT="''${XDG_DATA_DIRS}:${env.XDG_DATA_DIRS}"
                '';
              settings.readenv = 0;
            }
            {
              name = "systemd";
              control = "optional";
              modulePath = "${config.systemd.package}/lib/security/pam_systemd.so";
            }
            {
              name = "keyinit";
              control = "optional";
              modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
              settings.force = true;
              settings.revoke = true;
            }
            {
              name = "permit";
              control = "optional";
              modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
            }
          ];
        };
      };

      gdm-password = {
        useDefaultRules = false;
        rules = {
          auth = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "substack";
              modulePath = "login";
            }
          ];
          account = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "include";
              modulePath = "login";
            }
          ];
          password = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "substack";
              modulePath = "login";
            }
          ];
          session = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "include";
              modulePath = "login";
            }
          ];
        };
      };

      gdm-autologin = {
        useDefaultRules = false;
        rules = {
          auth = utils.pam.autoOrderRules [
            {
              name = "nologin";
              control = "requisite";
              modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
            }
            {
              name = "gdm-normal-user";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_succeed_if.so";
              settings.quiet = true;
              args = lib.mkBefore [
                "uid"
                ">="
                "1000"
              ];
            }
            {
              name = "gdm";
              enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
              control = "[success=ok default=1]";
              modulePath = "${gdm}/lib/security/pam_gdm.so";
            }
            {
              name = "gnome_keyring";
              enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
              control = "optional";
              modulePath = "${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so";
            }
            {
              name = "permit";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_permit.so";
            }
          ];

          account = utils.pam.autoOrderRules [
            {
              name = "unix";
              control = "sufficient";
              modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
            }
          ];

          password = utils.pam.autoOrderRules [
            {
              name = "unix";
              control = "requisite";
              modulePath = "${config.security.pam.package}/lib/security/pam_unix.so";
              settings.nullok = true;
              settings.yescrypt = true;
            }
          ];

          session = utils.pam.autoOrderRules [
            {
              name = "keyinit";
              control = "optional";
              modulePath = "${config.security.pam.package}/lib/security/pam_keyinit.so";
              settings.revoke = true;
            }
            {
              name = "login";
              control = "include";
              modulePath = "login";
            }
          ];
        };
      };

      # This would block password prompt when included by gdm-password.
      # GDM will instead run gdm-fingerprint in parallel.
      login.fprintAuth = lib.mkIf config.services.fprintd.enable false;

      gdm-fingerprint = lib.mkIf config.services.fprintd.enable {
        useDefaultRules = false;
        rules = {
          auth = utils.pam.autoOrderRules [
            {
              name = "shells";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_shells.so";
            }
            {
              name = "nologin";
              control = "requisite";
              modulePath = "${config.security.pam.package}/lib/security/pam_nologin.so";
            }
            {
              name = "faillock";
              control = "requisite";
              modulePath = "${config.security.pam.package}/lib/security/pam_faillock.so";
              settings.preauth = true;
            }
            {
              name = "fprintd";
              control = "required";
              modulePath = "${pkgs.fprintd}/lib/security/pam_fprintd.so";
            }
            {
              name = "env";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_env.so";
              settings.conffile = "/etc/pam/environment";
              settings.readenv = 0;
            }
            {
              name = "gdm";
              enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
              control = "[success=ok default=1]";
              modulePath = "${gdm}/lib/security/pam_gdm.so";
            }
            {
              name = "gnome_keyring";
              enable = pamLogin.enable && pamLogin.enableGnomeKeyring;
              control = "optional";
              modulePath = "${pkgs.gnome-keyring}/lib/security/pam_gnome_keyring.so";
            }
          ];

          account = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "include";
              modulePath = "login";
            }
          ];

          password = utils.pam.autoOrderRules [
            {
              name = "deny";
              control = "required";
              modulePath = "${config.security.pam.package}/lib/security/pam_deny.so";
            }
          ];

          session = utils.pam.autoOrderRules [
            {
              name = "login";
              control = "include";
              modulePath = "login";
            }
          ];
        };
      };
    };

  };

}
