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

with lib;
let
  cfg = config.services.tayga;

  # Converts an address set to a string
  strAddr = addr: "${addr.address}/${toString addr.prefixLength}";

  configFile = pkgs.writeText "tayga.conf" ''
    tun-device ${cfg.tunDevice}

    ipv4-addr ${cfg.ipv4.address}
    ${optionalString (cfg.ipv6.address != null) "ipv6-addr ${cfg.ipv6.address}"}

    prefix ${strAddr cfg.ipv6.pool}
    dynamic-pool ${strAddr cfg.ipv4.pool}
    data-dir ${cfg.dataDir}

    ${concatStringsSep "\n" (mapAttrsToList (ipv4: ipv6: "map " + ipv4 + " " + ipv6) cfg.mappings)}

    ${optionalString ((builtins.length cfg.log) > 0) ''
      log ${concatStringsSep " " cfg.log}
    ''}

    wkpf-strict ${boolToYesNo cfg.wkpfStrict}
  '';

  addrOpts =
    v:
    assert v == 4 || v == 6;
    {
      options = {
        address = mkOption {
          type = types.str;
          description = "IPv${toString v} address.";
        };

        prefixLength = mkOption {
          type = types.ints.between 0 (if v == 4 then 32 else 128);
          description = ''
            Subnet mask of the interface, specified as the number of
            bits in the prefix ("${if v == 4 then "24" else "64"}").
          '';
        };
      };
    };

  versionOpts = v: {
    options = {
      router = {
        address = mkOption {
          type = types.str;
          description = "The IPv${toString v} address of the router.";
        };
      };

      address = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = "The source IPv${toString v} address of the TAYGA server.";
      };

      pool = mkOption {
        type = with types; nullOr (submodule (addrOpts v));
        description = "The pool of IPv${toString v} addresses which are used for translation.";
      };
    };
  };
in
{
  options = {
    services.tayga = {
      enable = mkEnableOption "Tayga";

      package = mkPackageOption pkgs "tayga" { };

      ipv4 = mkOption {
        type = types.submodule (versionOpts 4);
        description = "IPv4-specific configuration.";
        example = literalExpression ''
          {
            address = "192.0.2.0";
            router = {
              address = "192.0.2.1";
            };
            pool = {
              address = "192.0.2.1";
              prefixLength = 24;
            };
          }
        '';
      };

      ipv6 = mkOption {
        type = types.submodule (versionOpts 6);
        description = "IPv6-specific configuration.";
        example = literalExpression ''
          {
            address = "2001:db8::1";
            router = {
              address = "64:ff9b::1";
            };
            pool = {
              address = "64:ff9b::";
              prefixLength = 96;
            };
          }
        '';
      };

      dataDir = mkOption {
        type = types.path;
        default = "/var/lib/tayga";
        description = "Directory for persistent data.";
      };

      tunDevice = mkOption {
        type = types.str;
        default = "nat64";
        description = "Name of the nat64 tun device.";
      };

      mappings = mkOption {
        type = types.attrsOf types.str;
        default = { };
        description = "Static IPv4 -> IPv6 host mappings.";
        example = literalExpression ''
          {
            "192.168.5.42" = "2001:db8:1:4444::1";
            "192.168.5.43" = "2001:db8:1:4444::2";
            "192.168.255.2" = "2001:db8:1:569::143";
          }
        '';
      };

      log = mkOption {
        type = types.listOf types.str;
        default = [ ];
        description = "Packet errors to log (drop, reject, icmp, self)";
        example = literalExpression ''
          [ "drop" "reject" "icmp" "self" ]
        '';
      };

      wkpfStrict = mkOption {
        type = types.bool;
        default = true;
        description = "Enable restrictions on the use of the well-known prefix (64:ff9b::/96) - prevents translation of non-global IPv4 ranges when using the well-known prefix. Must be enabled for RFC 6052 compatibility.";
      };
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      {
        assertion = allUnique (attrValues cfg.mappings);
        message = "Neither the IPv4 nor the IPv6 addresses must be entered twice in the mappings.";
      }
    ];

    networking.interfaces."${cfg.tunDevice}" = {
      virtual = true;
      virtualType = "tun";
      virtualOwner = null;
      ipv4 = {
        addresses = [
          {
            address = cfg.ipv4.router.address;
            prefixLength = 32;
          }
        ];
        routes = [
          cfg.ipv4.pool
        ];
      };
      ipv6 = {
        addresses = [
          {
            address = cfg.ipv6.router.address;
            prefixLength = 128;
          }
        ];
        routes = [
          cfg.ipv6.pool
        ];
      };
    };

    environment.etc."tayga.conf".source = configFile;

    systemd.services.tayga = {
      description = "Stateless NAT64 implementation";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];

      reloadTriggers = [ configFile ];
      serviceConfig = {
        ExecStart = "${cfg.package}/bin/tayga -d --nodetach --config /etc/tayga.conf";
        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
        Restart = "always";

        # Hardening Score: 1.5
        ProtectHome = true;
        SystemCallFilter = [
          "@network-io"
          "@system-service"
          "~@privileged"
          "~@resources"
        ];
        ProtectKernelLogs = true;
        CapabilityBoundingSet = "";
        RestrictAddressFamilies = [
          "AF_INET"
          "AF_INET6"
          "AF_NETLINK"
        ];
        StateDirectory = "tayga";
        DynamicUser = true;
        MemoryDenyWriteExecute = true;
        RestrictRealtime = true;
        RestrictSUIDSGID = true;
        ProtectHostname = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        RestrictNamespaces = true;
        NoNewPrivileges = true;
        ProtectControlGroups = true;
        SystemCallArchitectures = "native";
        PrivateTmp = true;
        LockPersonality = true;
        ProtectSystem = true;
        PrivateUsers = true;
        ProtectProc = "invisible";
      };
    };
  };
}
