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

let
  cfg = config.shb.lldap;

  fqdn = "${cfg.subdomain}.${cfg.domain}";

  inherit (lib) mkOption types;

  ensureFormat = pkgs.formats.json { };

  ensureFieldsOptions = name: {
    name = mkOption {
      type = types.str;
      description = "Name of the field.";
      default = name;
    };

    attributeType = mkOption {
      type = types.enum [
        "STRING"
        "INTEGER"
        "JPEG"
        "DATE_TIME"
      ];
      description = "Attribute type.";
    };

    isEditable = mkOption {
      type = types.bool;
      description = "Is field editable.";
      default = true;
    };

    isList = mkOption {
      type = types.bool;
      description = "Is field a list.";
      default = false;
    };

    isVisible = mkOption {
      type = types.bool;
      description = "Is field visible in UI.";
      default = true;
    };
  };
in
{
  imports = [
    ../../lib/module.nix
    ./mitmdump.nix

    (lib.mkRenamedOptionModule [ "shb" "ldap" ] [ "shb" "lldap" ])
  ];

  options.shb.lldap = {
    enable = lib.mkEnableOption "the LDAP service";

    dcdomain = lib.mkOption {
      type = lib.types.str;
      description = "dc domain to serve.";
      example = "dc=mydomain,dc=com";
    };

    subdomain = lib.mkOption {
      type = lib.types.str;
      description = "Subdomain under which the LDAP service will be served.";
      example = "grafana";
    };

    domain = lib.mkOption {
      type = lib.types.str;
      description = "Domain under which the LDAP service will be served.";
      example = "mydomain.com";
    };

    ldapPort = lib.mkOption {
      type = lib.types.port;
      description = "Port on which the server listens for the LDAP protocol.";
      default = 3890;
    };

    ssl = lib.mkOption {
      description = "Path to SSL files";
      type = lib.types.nullOr shb.contracts.ssl.certs;
      default = null;
    };

    webUIListenPort = lib.mkOption {
      type = lib.types.port;
      description = "Port on which the web UI is exposed.";
      default = 17170;
    };

    ldapUserPassword = lib.mkOption {
      description = "LDAP admin user secret. Must be >= 8 characters.";
      type = lib.types.submodule {
        options = shb.contracts.secret.mkRequester {
          mode = "0440";
          owner = "lldap";
          group = "lldap";
          restartUnits = [ "lldap.service" ];
        };
      };
    };

    jwtSecret = lib.mkOption {
      description = "JWT secret.";
      type = lib.types.submodule {
        options = shb.contracts.secret.mkRequester {
          mode = "0440";
          owner = "lldap";
          group = "lldap";
          restartUnits = [ "lldap.service" ];
        };
      };
    };

    restrictAccessIPRange = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      description = "Set a local network range to restrict access to the UI to only those IPs.";
      example = "192.168.1.1/24";
      default = null;
    };

    debug = lib.mkOption {
      description = "Enable debug logging.";
      type = lib.types.bool;
      default = false;
    };

    mount = lib.mkOption {
      type = shb.contracts.mount;
      description = ''
        Mount configuration. This is an output option.

        Use it to initialize a block implementing the "mount" contract.
        For example, with a zfs dataset:

        ```
        shb.zfs.datasets."ldap" = {
          poolName = "root";
        } // config.shb.lldap.mount;
        ```
      '';
      readOnly = true;
      default = {
        path = "/var/lib/lldap";
      };
    };

    backup = lib.mkOption {
      description = ''
        Backup configuration.
      '';
      type = lib.types.submodule {
        options = shb.contracts.backup.mkRequester {
          # TODO: is there a workaround that avoid needing to use root?
          # root because otherwise we cannot access the private StateDiretory
          user = "root";
          # /private because the systemd service uses DynamicUser=true
          sourceDirectories = [
            "/var/lib/private/lldap"
          ];
        };
      };
    };

    ensureUsers = mkOption {
      description = ''
        Create the users defined here on service startup.

        If `enforceUsers` option is `true`, the groups
        users belong to must be present in the `ensureGroups` option.

        Non-default options must be added to the `ensureGroupFields` option.
      '';
      default = { };
      type = types.attrsOf (
        types.submodule (
          { name, ... }:
          {
            freeformType = ensureFormat.type;

            options = {
              id = mkOption {
                type = types.str;
                description = "Username.";
                default = name;
              };

              email = mkOption {
                type = types.str;
                description = "Email.";
              };

              password = mkOption {
                description = "Password.";
                type = lib.types.submodule {
                  options = shb.contracts.secret.mkRequester {
                    mode = "0440";
                    owner = "lldap";
                    group = "lldap";
                    restartUnits = [ "lldap.service" ];
                  };
                };
              };

              displayName = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Display name.";
              };

              firstName = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "First name.";
              };

              lastName = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Last name.";
              };

              avatar_file = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Avatar file. Must be a valid path to jpeg file (ignored if avatar_url specified)";
              };

              avatar_url = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Avatar url. must be a valid URL to jpeg file (ignored if gravatar_avatar specified)";
              };

              gravatar_avatar = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Get avatar from Gravatar using the email.";
              };

              weser_avatar = mkOption {
                type = types.nullOr types.str;
                default = null;
                description = "Convert avatar retrieved by gravatar or the URL.";
              };

              groups = mkOption {
                type = types.listOf types.str;
                default = [ ];
                description = "Groups the user would be a member of (all the groups must be specified in group config files).";
              };
            };
          }
        )
      );
    };

    ensureGroups = mkOption {
      description = ''
        Create the groups defined here on service startup.

        Non-default options must be added to the `ensureGroupFields` option.
      '';
      default = { };
      type = types.attrsOf (
        types.submodule (
          { name, ... }:
          {
            freeformType = ensureFormat.type;

            options = {
              name = mkOption {
                type = types.str;
                description = "Name of the group.";
                default = name;
              };
            };
          }
        )
      );
    };

    ensureUserFields = mkOption {
      description = "Extra fields for users";
      default = { };
      type = types.attrsOf (
        types.submodule (
          { name, ... }:
          {
            options = ensureFieldsOptions name;
          }
        )
      );
    };

    ensureGroupFields = mkOption {
      description = "Extra fields for groups";
      default = { };
      type = types.attrsOf (
        types.submodule (
          { name, ... }:
          {
            options = ensureFieldsOptions name;
          }
        )
      );
    };

    enforceUsers = mkOption {
      description = "Delete users not set declaratively.";
      type = types.bool;
      default = false;
    };

    enforceUserMemberships = mkOption {
      description = "Remove users from groups not set declaratively.";
      type = types.bool;
      default = false;
    };

    enforceGroups = mkOption {
      description = "Remove groups not set declaratively.";
      type = types.bool;
      default = false;
    };

    dashboard = lib.mkOption {
      description = ''
        Dashboard contract consumer
      '';
      default = { };
      type = lib.types.submodule {
        options = shb.contracts.dashboard.mkRequester {
          externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
          externalUrlText = "https://\${config.shb.lldap.subdomain}.\${config.shb.lldap.domain}";
          internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}";
        };
      };
    };
  };

  config = lib.mkIf cfg.enable {

    services.nginx = {
      enable = true;

      virtualHosts.${fqdn} = {
        forceSSL = !(isNull cfg.ssl);
        sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
        sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
        locations."/" = {
          extraConfig = ''
            proxy_set_header Host $host;
          ''
          + (
            if isNull cfg.restrictAccessIPRange then
              ""
            else
              ''
                allow ${cfg.restrictAccessIPRange};
                deny all;
              ''
          );
          proxyPass = "http://${toString config.services.lldap.settings.http_host}:${toString config.shb.lldap.webUIListenPort}/";
        };
      };
    };

    users.users.lldap = {
      name = "lldap";
      group = "lldap";
      isSystemUser = true;
    };
    users.groups.lldap = { };

    services.lldap = {
      enable = true;

      inherit (cfg) enforceUsers enforceUserMemberships enforceGroups;

      environment = {
        RUST_LOG = lib.mkIf cfg.debug "debug";
      };

      settings = {
        http_url = "https://${fqdn}";
        http_host = "127.0.0.1";
        http_port = if !cfg.debug then cfg.webUIListenPort else cfg.webUIListenPort + 1;

        ldap_host = "127.0.0.1";
        ldap_port = cfg.ldapPort; # Would be great to be able to inspect this but it requires tcpdump instead of mitmproxy.
        ldap_base_dn = cfg.dcdomain;

        ldap_user_pass_file = toString cfg.ldapUserPassword.result.path;
        force_ldap_user_pass_reset = "always";
        jwt_secret_file = toString cfg.jwtSecret.result.path;

        verbose = cfg.debug;
      };

      inherit (cfg) ensureGroups ensureUserFields ensureGroupFields;
      ensureUsers = lib.mapAttrs (
        n: v:
        (lib.removeAttrs v [ "password" ])
        // {
          "password_file" = toString v.password.result.path;
        }
      ) cfg.ensureUsers;
    };

    # Harden lldap following https://github.com/NixOS/nixpkgs/pull/487933
    systemd.services.lldap.serviceConfig = {
      RemoveIPC = true;
      RestrictNamespaces = true;
      RestrictRealtime = true;
      RestrictSUIDSGID = true;
      RestrictAddressFamilies = [
        "AF_UNIX"
        "AF_INET"
        "AF_INET6"
      ];
      SystemCallFilter = [
        "@system-service"
        "~@privileged"
        "~@resources"
      ];
      SystemCallArchitectures = "native";
      CapabilityBoundingSet = "";
      LockPersonality = true;
      NoNewPrivileges = true;
      PrivateTmp = true;
      PrivateDevices = true;
      ProtectClock = true;
      ProtectControlGroups = true;
      ProtectHome = true;
      ProtectHostname = true;
      ProtectKernelLogs = true;
      ProtectKernelModules = true;
      ProtectKernelTunables = true;
      ProtectSystem = "strict";
      ProtectProc = "invisible";
      ProcSubset = "pid";
      MemoryDenyWriteExecute = true;
    };

    shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
      listenPort = config.shb.lldap.webUIListenPort;
      upstreamPort = config.shb.lldap.webUIListenPort + 1;
      after = [ "lldap.service" ];
      enabledAddons = [ config.shb.mitmdump.addons.logger ];
      extraArgs = [
        "--set"
        "verbose_pattern=/api"
      ];
    };
  };
}
