{ pkgs, lib, config, ... }:
with lib;
let
  inherit (config.users) users;
  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 = {
  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 = {
  systemd.services = listToAttrs (map (r:
    nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [
      { description = "UPnP ${toString r.internalPort}";
        after = [ "network-online.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";
          RestartSec = mkDefault r.maintainPeriod;
          DynamicUser = true;
          User = users."upnpc".name;
        };
      }
      r.service
    ])
    ) cfg.redirections);

  # This enables to match on the uid in the firewall.
  users.users."upnpc".isSystemUser = true;
};
meta.maintainers = with maintainers; [ julm ];
}