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

let
  inherit (builtins) map removeAttrs;
  inherit (lib) mapAttrs flatten concatMap concatMapStringsSep;

  cfg = config.plusultra.services.dex;

  process-client-settings = client:
    if client ? secretFile then
      (removeAttrs client [ "secretFile" ])
      // { secret = client.secretFile; }
    else
      client;

  settings =
    mapAttrs
      (name: value:
        if name == "staticClients" then
          map process-client-settings value
        else
          value
      )
      (cfg.settings // {
        storage = (cfg.settings.storage or { }) // {
          type = cfg.settings.storage.type or "sqlite3";
          config = cfg.settings.storage.config or {
            file = "${cfg.stateDir}/dex.db";
          };
        };
      });

  secret-files = concatMap
    (client:
      if client ? secretFile then
        [ client.secretFile ]
      else
        [ ]
    )
    (settings.staticClients or [ ]);

  format = pkgs.formats.yaml { };

  configYaml = format.generate "config.yaml" settings;

  replace-config-secrets = pkgs.writeShellScript "replace-config-secrets"
    (concatMapStringsSep "\n"
      (file: "${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' ${cfg.stateDir}/config.yaml")
      secret-files
    );

in
{
  options.plusultra.services.dex = {
    enable = lib.mkEnableOption "Dex, the OpenID Connect and OAuth 2 identity provider";

    stateDir = lib.mkOption {
      type = lib.types.path;
      default = "/var/lib/dex";
      description = "The state directory where config and data are stored.";
    };

    user = lib.mkOption {
      type = lib.types.str;
      default = "dex";
      description = "The user to run Dex as.";
    };

    group = lib.mkOption {
      type = lib.types.str;
      default = "dex";
      description = "The group to run Dex as.";
    };

    settings = lib.mkOption {
      type = format.type;
      default = { };
      example = lib.literalExpression ''
        {
          # External url
          issuer = "http://127.0.0.1:5556/dex";
          storage = {
            type = "postgres";
            config.host = "/var/run/postgres";
          };
          web = {
            http = "127.0.0.1:5556";
          };
          enablePasswordDB = true;
          staticClients = [
            {
              id = "oidcclient";
              name = "Client";
              redirectURIs = [ "https://example.com/callback" ];

              # The content of `secretFile` will be written into to the config as `secret`.
              secretFile = "/etc/dex/oidcclient";
            }
          ];
        }
      '';
      description = lib.mdDoc ''
        The available options can be found in
        [the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
      '';
    };
  };

  config = lib.mkIf cfg.enable {
    users = {
      users = lib.optionalAttrs (cfg.user == "dex") {
        dex = {
          group = cfg.group;
          home = cfg.stateDir;
          isSystemUser = true;
        };
      };

      groups = lib.optionalAttrs (cfg.group == "dex") {
        dex = { };
      };
    };

    systemd = {
      tmpfiles.rules = [
        "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
      ];

      services = {
        dex = {
          description = "dex identity provider";
          wantedBy = [ "multi-user.target" ];
          after = [ "networking.target" ];

          preStart = ''
            cp --remove-destination ${configYaml} ${cfg.stateDir}/config.yaml

            chmod 600 ${cfg.stateDir}/config.yaml

            ${replace-config-secrets}
          '';

          serviceConfig = {
            ExecStart = "${pkgs.dex-oidc}/bin/dex serve ${cfg.stateDir}/config.yaml";
            WorkingDirectory = cfg.stateDir;

            User = cfg.user;
            Group = cfg.group;

            AmbientCapabilities = "CAP_NET_BIND_SERVICE";
            BindReadOnlyPaths = [
              "/nix/store"
              "-/etc/resolv.conf"
              "-/etc/nsswitch.conf"
              "-/etc/hosts"
              "-/etc/localtime"
              "-/etc/dex"
            ];
            BindPaths = [ cfg.stateDir ] ++ lib.optional (settings.storage.type == "postgres") "/var/run/postgresql";
            CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
            ## ProtectClock= adds DeviceAllow=char-rtc r
            #DeviceAllow = "";
            #DynamicUser = true;
            #LockPersonality = true;
            #MemoryDenyWriteExecute = true;
            #NoNewPrivileges = true;
            #PrivateDevices = true;
            #PrivateMounts = true;
            ## Port needs to be exposed to the host network
            ##PrivateNetwork = true;
            #PrivateTmp = true;
            #PrivateUsers = true;
            #ProcSubset = "pid";
            #ProtectClock = true;
            #ProtectHome = true;
            #ProtectHostname = true;
            ## Would re-mount paths ignored by temporary root
            ##ProtectSystem = "strict";
            #ProtectControlGroups = true;
            #ProtectKernelLogs = true;
            #ProtectKernelModules = true;
            #ProtectKernelTunables = true;
            #ProtectProc = "invisible";
            #RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
            #RestrictNamespaces = true;
            #RestrictRealtime = true;
            #RestrictSUIDSGID = true;
            #SystemCallArchitectures = "native";
            #SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
            #TemporaryFileSystem = "/:ro";
            # Does not work well with the temporary root
            #UMask = "0066";
          };
        };
      };
    };
  };
}
