{ config, lib, ... }@args:

let
  inherit (lib)
    mkEnableOption
    mkDefault
    mkIf
    mkOption
    ;

  inherit (lib.types)
    attrs
    attrsOf
    ints
    listOf
    nullOr
    str
    submodule
    unspecified
    ;

  addressType =
    max:
    submodule {
      options = {
        address = mkOption {
          type = str;
          description = "IP address.";
        };
        prefixLength = mkOption {
          type = ints.between 8 max;
          description = "Length of the prefix used in the local network.";
        };
      };
    };

  org = config.organization;
in

{
  options = {
    organization = {
      members = mkOption {
        type = attrsOf (submodule {
          options = {
            name = mkOption {
              type = str;
              description = ''
                Name of the member.
              '';
            };

            email = mkOption {
              type = str;
              description = ''
                Main e-mail address of the member.
              '';
            };
          };
        });

        description = ''
          Members of the DGNum organization.
        '';
      };

      groups = mkOption {
        type = attrsOf (listOf str);
        description = ''
          Groups of the DGNum organization.
        '';
      };
    };

    nodes = mkOption {
      type = attrsOf (
        submodule (
          { config, name, ... }:
          {
            options = {
              deployment = mkOption {
                type = attrs;
                default = { };
              };

              stateVersion = mkOption {
                type = str;
                description = ''
                  State version of the node.
                '';
              };

              nixpkgs = mkOption {
                type = str;
                inherit (import ./nixpkgs.nix) default;
                description = ''
                  Version of nixpkgs to use.
                '';
              };

              nix-modules = mkOption {
                type = listOf str;
                default = [ ];
                description = ''
                  List of modules to import from [nix-modules](https://git.hubrecht.ovh/hubrecht/nix-modules).
                '';
              };

              hashedPassword = mkOption {
                type = str;
                description = ''
                  The hashed password for the root account.
                '';
              };

              admins = mkOption {
                type = listOf str;
                default = [ ];
                description = ''
                  List of members to be given root access to this node.
                '';
              };

              adminGroups = mkOption {
                type = listOf str;
                default = [ ];
                description = ''
                  List of groups to be given root access to this node.
                '';
              };

              site = mkOption {
                type = str;
                description = ''
                  Geographical site where the node is located.
                '';
              };

              vm-cluster = mkOption {
                type = nullOr str;
                default = null;
                description = "VM cluster where the VM is located";
              };
            };

            config = {
              deployment = {
                tags = [ "infra-${config.site}" ];
                targetHost =
                  let
                    ip = with args.config.network.${name}.addresses; ipv4 ++ ipv6;
                  in
                  mkIf (ip != [ ]) (mkDefault (builtins.head ip));
              };
            };
          }
        )
      );
      description = ''
        Nodes of the infrastructure.
      '';
    };

    network = mkOption {
      type = attrsOf (
        submodule (
          { config, ... }:
          {
            options = {
              interfaces = mkOption {
                type = attrsOf (
                  submodule (
                    { config, ... }:
                    {
                      options = {
                        ipv4 = mkOption {
                          type = listOf (addressType 32);
                          default = [ ];
                          description = ''
                            List of ipv4 addresses assigned to the interface.
                          '';
                        };

                        ipv6 = mkOption {
                          type = listOf (addressType 64);
                          default = [ ];
                          description = ''
                            List of ipv6 addresses assigned to the interface.
                          '';
                        };

                        gateways = mkOption {
                          type = listOf str;
                          description = ''
                            List of gateways used by the interface.
                          '';
                        };

                        DHCP = mkOption {
                          type = nullOr str;
                          default = null;
                          description = "Whether to enable DHCP on the interface.";
                        };

                        dns = mkOption {
                          type = listOf str;
                          default = [ ];
                        };

                        enableDefaultDNS = mkEnableOption "default DNS servers.";
                      };

                      config.dns = mkIf config.enableDefaultDNS [
                        "1.1.1.1#cloudflare-dns.com"
                        "8.8.8.8#dns.google"
                        "1.0.0.1#cloudflare-dns.com"
                        "8.8.4.4#dns.google"
                        "2606:4700:4700::1111#cloudflare-dns.com"
                        "2001:4860:4860::8888#dns.google"
                        "2606:4700:4700::1001#cloudflare-dns.com"
                        "2001:4860:4860::8844#dns.google"
                      ];
                    }
                  )
                );
              };

              addresses = {
                ipv4 = mkOption {
                  type = listOf str;
                  default = [ ];
                  description = ''
                    List of public ipv4 addresses of the node.
                  '';
                };

                ipv6 = mkOption {
                  type = listOf str;
                  default = [ ];
                  description = ''
                    List of public ipv6 addresses of the node.
                  '';
                };
              };

              hostId = mkOption {
                type = str;
                description = ''
                  Network Id of the node.
                '';
              };

              netbirdIp = mkOption {
                type = nullOr str;
                description = ''
                  IP address of the node in the netbird network.
                '';
              };
            };

            config =
              let
                getAddresses =
                  version: builtins.concatMap (int: builtins.map (builtins.getAttr "address") int.${version});
              in
              {
                addresses = {
                  ipv4 = builtins.filter (ip: builtins.substring 0 7 ip != "192.168") (
                    getAddresses "ipv4" (builtins.attrValues config.interfaces)
                  );
                  ipv6 = builtins.filter (_: true) ((getAddresses "ipv6") (builtins.attrValues config.interfaces));
                };
              };
          }
        )
      );
      description = ''
        Network configuration for the different machines.
      '';
    };

    assertions = mkOption {
      type = listOf unspecified;
      internal = true;
      default = [ ];
      description = ''
        This option allows modules to express conditions that must
        hold for the evaluation of the system configuration to
        succeed, along with associated error messages for the user.
      '';
    };
  };

  config =
    let
      members = builtins.attrNames org.members;
      groups = builtins.attrNames org.groups;

      nameExists =
        list: f: groups:
        builtins.attrValues (
          builtins.mapAttrs (name: members: {
            assertion = builtins.all (x: builtins.elem x list) members;
            message = f name;
          }) groups
        );

      membersExists = nameExists members;
      groupsExists = nameExists groups;

      extract = name: builtins.mapAttrs (_: builtins.getAttr name);
    in
    {
      assertions = builtins.concatLists [
        # Check that all group members exist
        (membersExists (
          name: "A member of the ${name} group was not found in the members list."
        ) org.groups)

        # Check that all node admins exist
        (membersExists (name: "A member of the node ${name} admins was not found in the members list.") (
          extract "admins" config.nodes
        ))

        # Check that all node adminGroups exist
        (groupsExists (name: "A member of the node ${name} adminGroups was not found in the groups list.") (
          extract "adminGroups" config.nodes
        ))

        # Check that all members have ssh keys
        (builtins.map (name: {
          assertion = ((import ../keys)._keys.${name} or [ ]) != [ ];
          message = "No ssh keys found for ${name}.";
        }) members)
      ];
    };
}