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

with lib;
let

  cfg = config.fudo.postgresql;

  userOpts = { username, ... }: {
    options = {
      password = mkOption {
        type = with types; nullOr str;
        description = "The user's (plaintext) password.";
        default = null;
      };

      databases = mkOption {
        type = with types; loaOf str;
        description = "Map of databases to which this user has access, to the required perms.";
        default = {};
        example = {
          my_database = "ALL PRIVILEGES";
        };
      };
    };
  };

  databaseOpts = { dbname, ... }: {
    options = {
      users = mkOption {
        type = with types; listOf str;
        description = "A list of users who should have full access to this database.";
        default = [];
      };
    };
  };

  userDatabaseAccess = user: databases:
    mapAttrs' (database: perms:
      nameValuePair "DATABASE ${database}" perms)
      databases;

  stringJoin = joiner: els:
    if (length els) == 0 then
      ""
    else
      foldr(lel: rel: "${lel}${joiner}${rel}") (last els) (init els);

  makeEntry = nw:
    "host     all             all             ${nw} gss include_realm=0 krb_realm=FUDO.ORG";

  makeNetworksEntry = networks:
    stringJoin "\n" (map makeEntry networks);

  setPasswordSql = username: attrs:
    "ALTER USER ${username} ENCRYPTED PASSWORD '${attrs.password}';";

  setPasswordsSql = users:
    (stringJoin "\n"
      (mapAttrsToList (username: attrs: setPasswordSql username attrs)
        (filterAttrs (user: attrs: attrs.password != null) users))) + "\n";

  makeLocalUserPasswordEntries = users:
    stringJoin "\n"
      (mapAttrsToList
        (username: attrs:
          stringJoin "\n"
            (map (db: ''
              local  ${db}  ${username}   md5
              host   ${db}  ${username}   127.0.0.1/16   md5
              host   ${db}  ${username}   ::1/128        md5
            '') (attrNames attrs.databases)))
        users);


in {

  options.fudo.postgresql = {
    enable = mkEnableOption "Fudo PostgreSQL Server";

    ssl-private-key = mkOption {
      type = types.str;
      description = "Location of the server SSL private key.";
    };

    ssl-certificate = mkOption {
      type = types.str;
      description = "Location of the server SSL certificate.";
    };

    keytab = mkOption {
      type = types.str;
      description = "Location of the server Kerberos keytab.";
    };

    local-networks = mkOption {
      type = with types; listOf str;
      description = "A list of networks from which to accept connections.";
      example = [
        "10.0.0.1/16"
      ];
      default = [];
    };

    users = mkOption {
      type = with types; loaOf (submodule userOpts);
      description = "A map of users to user attributes.";
      example = {
        sampleUser = {
          password = "some-password";
          databases = [ "sample_user_db" ];
        };
      };
      default = {};
    };

    databases = mkOption {
      type = with types; loaOf (submodule databaseOpts);
      description = "A map of databases to database options.";
      default = {};
    };

    socket-directory = mkOption {
      type = types.str;
      description = "Directory in which to place unix sockets.";
      default = "/run/postgresql";
    };

    socket-group = mkOption {
      type = types.str;
      description = "Group for accessing sockets.";
      default = "postgres_local";
    };

    local-users = mkOption {
      type = with types; listOf str;
      description = "Users able to access the server via local socket.";
      default = [];
    };
  };

  config = mkIf cfg.enable {

    environment = {
      systemPackages = with pkgs; [
        postgresql_11_gssapi
      ];

      etc = {
        "postgresql/private/privkey.pem" = {
          mode = "0400";
          user = "postgres";
          group = "postgres";
          source = cfg.ssl-private-key;
        };

        "postgresql/cert.pem" = {
          mode = "0444";
          user = "postgres";
          group = "postgres";
          source = cfg.ssl-certificate;
        };

        "postgresql/private/postgres.keytab" = {
          mode = "0400";
          user = "postgres";
          group = "postgres";
          source = cfg.keytab;
        };

        "postgresql/private/user-script.sql" = {
          mode = "0400";
          user = "postgres";
          group = "postgres";
          text = setPasswordsSql cfg.users;
        };
      };
    };

    users.groups = {
      ${cfg.socket-group} = {
        members = ["postgres"] ++ cfg.local-users;
      };
    };

    services.postgresql = {
      enable = true;
      package = pkgs.postgresql_11_gssapi;
      enableTCPIP = true;
      ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
      ensureUsers = mapAttrsToList
        (username: attrs:
          {
            name = username;
            ensurePermissions =
              #{ "DATABASE ${username}" = "ALL PRIVILEGES"; };
              (userDatabaseAccess username attrs.databases);
          })
        cfg.users;

      extraConfig = ''
        krb_server_keyfile = '/etc/postgresql/private/postgres.keytab'

        ssl = true
        ssl_cert_file = '/etc/postgresql/cert.pem'
        ssl_key_file = '/etc/postgresql/private/privkey.pem'

        unix_socket_directories = '${cfg.socket-directory}'
        unix_socket_group = '${cfg.socket-group}'
        unix_socket_permissions = 0777
      '';

      authentication = lib.mkForce ''
        ${makeLocalUserPasswordEntries cfg.users}

        local   all              all             ident

        # host-local
        host    all              all             127.0.0.1/32            gss include_realm=0 krb_realm=FUDO.ORG
        host    all              all             ::1/128                 gss include_realm=0 krb_realm=FUDO.ORG

        # local networks
        ${makeNetworksEntry cfg.local-networks}
      '';

      # initialScript = pkgs.writeText "database-init.sql" ''
      #   ${setPasswordsSql cfg.users}
      # '';
    };

    systemd.services.postgresql.postStart = ''
      ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -f /etc/postgresql/private/user-script.sql -d postgres
      ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
    '';
  };
}