{ pkgs, lib, config, ... }:
with lib;
let
  inherit (config.users) users groups;
  cfg = config.services.upnpc;
  getInfo = ''
    while IFS=: read -r k v; do
      k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
      v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
      case $k in
        (desc) desc=$v;;
        ("Local LAN ip address") localIP=$v;;
      esac
    done <<EOF
    $(upnpc -s)
    EOF
  '';
in
{
  options.services.upnpc = {
    enable = mkEnableOption "UPnP redirections";
    redirections = mkOption {
      description = "UPnP redirections to request.";
      default = [ ];
      type = types.listOf (types.submodule ({ config, ... }: {
        options.externalPort = mkOption {
          description = "External port to open on the redirecting device.";
          type = types.port;
        };
        options.internalPort = mkOption {
          description = "Internal port, target of the redirection.";
          type = types.port;
          default = config.externalPort;
        };
        options.protocol = mkOption {
          description = "Protocol to redirect.";
          type = with types; enum [ "TCP" "UDP" ];
          default = "TCP";
        };
        options.description = mkOption {
          description = "Description of the port mapping";
          type = types.str;
          default = "";
        };
        options.duration = mkOption {
          description = "Duration of the redirection, in seconds. 0 means indefinitely.";
          type = types.int;
          default = 0;
        };
        options.maintainPeriod = mkOption {
          description = "Period (in seconds) between runs to maintain the redirection.";
          type = with types; nullOr int;
          default = if config.duration > 0 then config.duration / 2 else null;
          defaultText = "if duration > 0 then duration / 2 else null";
        };
        options.override = mkOption {
          description = "Try to override the redirection in case of conflict in mapping entry.";
          type = types.bool;
          default = true;
        };
        options.service = mkOption {
          description = "Configuration specific to the systemd service handling this UPnP redirecting.";
          type = types.attrs;
          default = { };
        };
      }));
    };
  };
  config = mkIf cfg.enable {
    systemd.services = listToAttrs (map
      (r:
        nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [
          {
            description = "UPnP ${toString r.internalPort}";
            after = [ "network-pre.target" ];
            #wantedBy = [ "multi-user.target" ];
            path = [ pkgs.miniupnpc ];
            serviceConfig = {
              Type = if r.maintainPeriod == null then "oneshot" else "simple";
              RemainAfterExit = r.maintainPeriod == null;
              ExecStart = pkgs.writeShellScript "upnpc-start-${toString r.internalPort}" ''
                set -eu
                redirect () {
                  result=
                  while IFS= read -r line; do
                    echo >&2 -E "$line"
                    case $line in
                      (*" is redirected to internal $localIP:${toString r.internalPort}"*) result=ok ;;
                      (*ConflictInMappingEntry*) result=conflict ;;
                    esac
                  done <<EOF
                $(upnpc -u "$desc" ${optionalString (r.description != "") "-e \"${r.description}\""} \
                  -a "$localIP" ${toString r.internalPort} ${toString r.externalPort} ${r.protocol} ${toString r.duration} 2>&1)
                EOF
                }
                while true; do
                  ${getInfo}
                  redirect
                  ${optionalString r.override ''
                    test "$result" != conflict || {
                      upnpc -u "$desc" -d ${toString r.externalPort} ${r.protocol}
                      redirect
                    }
                  ''}
                  case $result in
                    (ok) ${if r.maintainPeriod == null then "break" else "sleep " + toString r.maintainPeriod} ;;
                    (*) exit 1 ;;
                  esac
                done
              '';
              ExecStop = "${pkgs.miniupnpc}/bin/upnpc -d ${toString r.externalPort} ${r.protocol}";
              Restart = "on-failure";
              DynamicUser = true;
              User = users."upnpc".name;
            } // lib.optionalAttrs (r.maintainPeriod != null) {
              RestartSec = mkDefault r.maintainPeriod;
            };
          }
          r.service
        ])
      )
      cfg.redirections);

    # This enables to match on the uid in the firewall.
    users.users."upnpc" = {
      isSystemUser = true;
      group = groups."upnpc".name;
    };
    users.groups."upnpc" = { };
    networking.nftables.ruleset =
      lib.optionalString (cfg.redirections != [ ]) ''
        table inet filter {
          # A set containing the udp port(s) to which SSDP replies are allowed.
          set upnpc-ssdp {
            type inet_service
            timeout 5s
          }
          chain input-net {
            # Create a rule for accepting any SSDP packets going to a remembered port.
            udp dport @upnpc-ssdp counter accept comment "SSDP answer"
          }
          chain output-net {
            skuid ${users.upnpc.name} \
              tcp dport ssdp \
              counter accept \
              comment "SSDP automatic opening"
            skuid ${users.upnpc.name} \
              ip daddr 239.255.255.250 udp dport ssdp \
              set add udp sport @upnpc-ssdp \
              comment "SSDP automatic opening"
            skuid ${users.upnpc.name} \
              ip daddr 239.255.255.250 udp dport ssdp \
              counter accept \
              comment "SSDP"
          }
        }
      '' + lib.optionalString config.networking.enableIPv6 ''
        table inet filter {
          chain output-net {
            skuid ${users.upnpc.name} \
              ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
              udp dport ssdp \
              set add udp sport @upnpc-ssdp \
              comment "SSDP automatic opening"
            skuid ${users.upnpc.name} \
              ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
              udp dport ssdp \
              counter accept comment "SSDP"
          }
        }
      '';
  };
  meta.maintainers = with maintainers; [ julm ];
}