{ config, lib, ... }:

let
  cfg = config.state;

  supportedProtocols = [ "tcp" "udp" ];

  portRangeType = {
    options = {
      from = lib.mkOption {
        type = lib.types.port;
        description = ''
          The start of the range of TCP/UDP ports to be taken over.
        '';
      };

      to = lib.mkOption {
        type = lib.types.port;
        description = ''
          The end of the range of TCP/UDP ports to be taken over.
        '';
      };
    };
  };

  portModule = { lib, ... }: {
    options = {
      protocols = lib.mkOption {
        type = with lib.types; listOf (enum supportedProtocols);
        description = ''
          Indicates the type of protocol of the service.
        '';
        default = [ "tcp" "udp" ];
        example = [ "tcp" ];
      };

      value = lib.mkOption {
        type = with lib.types; either port (submodule portRangeType);
        description = ''
          The port number itself.
        '';
      };

      openFirewall = lib.mkEnableOption "opening the ports to firewall";
    };
  };
in {
  options.state = let
    portsModule = { lib, ... }: {
      options = {
        ports = lib.mkOption {
          type = with lib.types; attrsOf (submodule portModule);
          description = ''
            A set of ports indicating what goes where in the NixOS system.
          '';
          default = { };
          example = lib.literalExpression ''
            rec {
              gonic = {
                value = 5757;
                protocols = [ "tcp" ];
                openFirewall = true;
              };
              uxplay = {
                value = 7864;
                openFirewall = true;
              };
              uxplayClients.value = {
                from = uxplay.value + 1;
                to = uxplay.value + 20;
              };
            }
          '';
        };
      };
    };
  in lib.mkOption { type = lib.types.submodule portsModule; };

  config = lib.mkIf (cfg.ports != { }) {
    networking.firewall = let
      allPortsToBeOpened = lib.filterAttrs (_: v: v.openFirewall) cfg.ports;
      hasProtocol = protocol: v: lib.elem protocol v.protocols;
      mkFirewallEntry = protocol: v:
        let inherit (v) value;
        in if lib.isAttrs value then {
          ${
            if protocol == "tcp" then
              "allowedTCPPortRanges"
            else
              "allowedUDPPortRanges"
          } = [ value ];
        } else {
          ${
            if protocol == "tcp" then "allowedTCPPorts" else "allowedUDPPorts"
          } = [ value ];
        };

      mkFirewallEntryModule = _: v:
        lib.optionalAttrs (hasProtocol "udp" v) (mkFirewallEntry "udp" v)
        // lib.optionalAttrs (hasProtocol "tcp" v) (mkFirewallEntry "tcp" v);
    in lib.mkMerge
    (lib.mapAttrsToList mkFirewallEntryModule allPortsToBeOpened);
  };
}
