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

with lib;
let

  cfg = config.fudo.auth.ldap-server;

  ldapSystemUserOpts = { name, ... }: {
    options = {
      description = mkOption {
        type = types.str;
        description = ''
          The description of this system user.
        '';
      };

      hashed-password = mkOption {
        type = types.str;
        description = ''
          The password for this user, hashed with ldappasswd.
        '';
        default = "";
      };
    };
  };

  ldapGroupOpts = { name, ... }: {
    options = {
      gid = mkOption {
        type = types.int;
        description = ''
          The GID number of this group.
        '';
      };

      description = mkOption {
        type = types.str;
        description = ''
          The description of this group.
        '';
      };

      members = mkOption {
        type = with types; listOf str;
        default = [ ];
        description = ''
          A list of usernames representing the members of this group.
        '';
      };
    };
  };

  ldapUserOpts = { name, ... }: {
    options = {

      uid = mkOption {
        type = types.int;
        description = ''
          The UID number of this user.
        '';
      };

      common-name = mkOption {
        type = types.str;
        description = ''
          The given name of this user.
        '';
      };

      group = mkOption {
        type = types.str;
        description = ''
          The name of the user's primary group.
        '';
      };

      login-shell = mkOption {
        type = types.str;
        default = "/bin/bash";
        description = ''
          The user's preferred shell. Default is /bin/bash.
        '';
      };

      description = mkOption {
        type = types.str;
        default = "Fudo Member";
        description = ''
          The description of this user.
        '';
      };

      hashed-password = mkOption {
        type = types.str;
        description = ''
          The password for this user, hashed with ldappasswd.
        '';
        default = "";
      };
    };
  };

  stringJoin = joiner: attrList:
    if (length attrList) == 0 then
      ""
    else
      foldr (lAttr: rAttr: "${lAttr}${joiner}${rAttr}") (last attrList)
      (init attrList);

  getUserGidNumber = user: group-map: group-map.${user.group}.gid;

  attrOr = attrs: attr: value: if attrs ? ${attr} then attrs.${attr} else value;

  mkHomeDir = username: user-opts:
    if (user-opts.group == "admin") then
      "/home/${username}"
    else
      "/home/${user-opts.group}/${username}";

  userLdif = base: name: group-map: opts: ''
    dn: uid=${name},ou=members,${base}
    uid: ${name}
    objectClass: account
    objectClass: shadowAccount
    objectClass: posixAccount
    cn: ${opts.common-name}
    uidNumber: ${toString (opts.uid)}
    gidNumber: ${toString (getUserGidNumber opts group-map)}
    homeDirectory: ${mkHomeDir name opts}
    description: ${opts.description}
    shadowLastChange: 12230
    shadowMax: 99999
    shadowWarning: 7
    userPassword: ${opts.hashed-password}
  '';

  systemUserLdif = base: name: opts: ''
    dn: cn=${name},${base}
    objectClass: organizationalRole
    objectClass: simpleSecurityObject
    cn: ${name}
    description: ${opts.description}
    userPassword: ${opts.hashed-password}
  '';

  toMemberList = userList:
    stringJoin "\n" (map (username: "memberUid: ${username}") userList);

  groupLdif = base: name: opts: ''
    dn: cn=${name},ou=groups,${base}
    objectClass: posixGroup
    cn: ${name}
    gidNumber: ${toString (opts.gid)}
    description: ${opts.description}
    ${toMemberList opts.members}
  '';

  systemUsersLdif = base: user-map:
    stringJoin "\n"
    (mapAttrsToList (name: opts: systemUserLdif base name opts) user-map);

  groupsLdif = base: group-map:
    stringJoin "\n"
    (mapAttrsToList (name: opts: groupLdif base name opts) group-map);

  usersLdif = base: group-map: user-map:
    stringJoin "\n"
    (mapAttrsToList (name: opts: userLdif base name group-map opts) user-map);

in {

  options = {
    fudo = {
      auth = {
        ldap-server = {
          enable = mkEnableOption "Fudo Authentication";

          kerberos-host = mkOption {
            type = types.str;
            description = ''
              The name of the host to use for Kerberos authentication.
            '';
          };

          kerberos-keytab = mkOption {
            type = types.str;
            description = ''
              The path to a keytab for the LDAP server, containing a principal for ldap/<hostname>.
            '';
          };

          sslCert = mkOption {
            type = types.str;
            description = ''
              The path to the SSL certificate to use for the server.
            '';
          };

          sslKey = mkOption {
            type = types.str;
            description = ''
              The path to the SSL key to use for the server.
            '';
          };

          sslCACert = mkOption {
            type = with types; nullOr str;
            description = ''
              The path to the SSL CA cert used to sign the certificate.
            '';
            default = null;
          };

          organization = mkOption {
            type = types.str;
            description = ''
              The name to use for the organization.
            '';
          };

          base = mkOption {
            type = types.str;
            description = ''
              The base dn of the LDAP server (eg. "dc=fudo,dc=org").
            '';
          };

          rootpw-file = mkOption {
            default = "";
            type = types.str;
            description = ''
              The path to a file containing the root password for this database.
            '';
          };

          listen-uris = mkOption {
            default = [ ];
            type = with types; listOf str;
            description = ''
              A list of URIs on which the ldap server should listen.
            '';
            example = [ "ldap://auth.fudo.org" "ldaps://auth.fudo.org" ];
          };

          users = mkOption {
            default = { };
            type = with types; attrsOf (submodule ldapUserOpts);
            example = {
              tester = {
                uid = 10099;
                common-name = "Joe Blow";
                hashed-password = "<insert password hash>";
              };
            };
            description = ''
              Users to be added to the Fudo LDAP database.
            '';
          };

          groups = mkOption {
            default = { };
            type = with types; attrsOf (submodule ldapGroupOpts);
            example = {
              admin = {
                gid = 1099;
                members = [ "tester" ];
              };
            };
            description = ''
              Groups to be added to the Fudo LDAP database.
            '';
          };

          system-users = mkOption {
            default = { };
            type = with types; attrsOf (submodule ldapSystemUserOpts);
            example = {
              replicator = {
                description = "System user for database sync";
                hashed-password = "<insert password hash>";
              };
            };
            description = ''
              System users to be added to the Fudo LDAP database.
            '';
          };
        };
      };
    };
  };

  config = mkIf cfg.enable {

    environment = {
      etc = {
        "openldap/sasl2/slapd.conf" = {
          mode = "0400";
          user = "openldap";
          group = "openldap";
          # FIXME: take arguments!
          text = ''
            mech_list: gssapi external
            keytab: /etc/ldap/ldap.keytab
          '';
        };
      };
    };

    systemd.services.openldap = {
      environment = { KRB5_KTNAME = cfg.kerberos-keytab; };
      # FIXME: THIS IS ALL UNPROVEN
      serviceConfig = {
        PrivateDevices = true;
        PrivateTmp = true;
        PrivateMounts = true;
        ProtectControlGroups = true;
        ProtectKernelTunables = true;
        ProtectKernelModules = true;
        ProtectSystem = true;
        ProtectHostname = true;
        ProtectHome = true;
        ProtectClock = true;
        ProtectKernelLogs = true;
        KeyringMode = "private";
        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
        Restart = "on-failure";
        LockPersonality = true;
        RestrictRealtime = true;
        MemoryDenyWriteExecute = true;
        SystemCallFilter =
          "~@clock @debug @module @mount @raw-io @reboot @swap @privileged @resources @cpu-emulation @obsolete";
        UMask = "7007";
        InaccessiblePaths = [ "/home" "/root" ];
        LimitNOFILE = 49152;
        PermissionsStartOnly = true;
      };
    };

    services.openldap = {
      enable = true;
      suffix = cfg.base;
      rootdn = "cn=admin,${cfg.base}";
      rootpwFile = "${cfg.rootpw-file}";
      urlList = cfg.listen-uris;

      extraConfig = ''

        TLSCertificateFile ${cfg.sslCert}
        TLSCertificateKeyFile ${cfg.sslKey}
        ${optionalString (cfg.sslCACert != null)
        "TLSCACertificateFile ${cfg.sslCACert}"}

        authz-regexp "^uid=auth/([^.]+)\.fudo\.org,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org"
        authz-regexp "^uid=[^,/]+/root,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=admin,dc=fudo,dc=org"
        authz-regexp "^uid=([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "uid=$1,ou=members,dc=fudo,dc=org"
        authz-regexp "^uid=host/([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org"
        authz-regexp "^gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth$" "cn=admin,dc=fudo,dc=org"

      '';

      extraDatabaseConfig = ''
        # access to dn=base=""
        #   by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
        #   by * read

        access to attrs=userPassword,shadowLastChange
          by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
          by group.exact="cn=admin,ou=members,${cfg.base}" write
          by dn.exact="cn=auth_reader,${cfg.base}" read
          by dn.exact="cn=replicator,${cfg.base}" read
          by self write
          by * auth

        access to dn.exact="cn=admin,ou=groups,${cfg.base}"
          by dn.exact="cn=admin,${cfg.base}" write
          by users read
          by * none

        access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid
          by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write
          by group.exact="cn=admin,ou=groups,${cfg.base}" write
          by users read
          by * none

        access to dn.subtree="ou=members,${cfg.base}" attrs=cn,sn,homeDirectory,loginShell,gecos,description,homeDirectory,uidNumber,gidNumber
          by group.exact="cn=admin,ou=groups,${cfg.base}" write
          by dn.exact="cn=user_db_reader,${cfg.base}" read
          by users read
          by * none

        access to dn.exact="cn=admin,ou=groups,${cfg.base}"
          by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
          by users read
          by * none

        access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid
          by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
          by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write
          by group.exact="cn=admin,ou=groups,${cfg.base}" write
          by users read
          by * none

        access to *
          by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
          by users read
          by * none


        index objectClass,uid eq
              '';

      declarativeContents = ''
        dn: ${cfg.base}
        objectClass: top
        objectClass: dcObject
        objectClass: organization
        o: ${cfg.organization}

        dn: ou=groups,${cfg.base}
        objectClass: organizationalUnit
        description: ${cfg.organization} groups

        dn: ou=members,${cfg.base}
        objectClass: organizationalUnit
        description: ${cfg.organization} members

        dn: cn=admin,${cfg.base}
        objectClass: organizationalRole
        cn: admin
        description: "Admin User"

        ${systemUsersLdif cfg.base cfg.system-users}
        ${groupsLdif cfg.base cfg.groups}
        ${usersLdif cfg.base cfg.groups cfg.users}
      '';
    };
  };
}