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

let
  inherit (pkgs) writeScript writeText;

  inherit (lib)
    concatMapStringsSep
    concatStringsSep
    attrNames
    unique
    fold
    any
    attrValues
    catAttrs
    filter
    flatten
    length
    hasAttr
    mkEnableOption
    mkOption
    mkIf
    types
    sort;

  elemIsIn = a: as:
    any (x: x == a) as;

  cfg = config.lass.iptables;

  out = {
    options.lass.iptables = api;
    config = mkIf cfg.enable imp;
  };

  api = {
    enable = mkEnableOption "iptables";

    #tables.filter.INPUT = {
    # policy = "DROP";
    # rules = [
    #   { predicate = "-i retiolum"; target = "ACCEPT"; priority = -10; }
    # ];
    #};
    #new api
    tables = mkOption {
      type = with types; attrsOf (attrsOf (submodule ({
        options = {
          policy = mkOption {
            type = str;
            default = "-";
          };
          rules = mkOption {
            type = nullOr (listOf (submodule ({
              options = {
                predicate = mkOption {
                  type = str;
                };
                target = mkOption {
                  type = str;
                };
                precedence = mkOption {
                  type = int;
                  default = 0;
                };
              };
            })));
            default = null;
          };
        };
      })));
    };
  };

  imp = {
    networking.firewall.enable = false;

    systemd.services.lass-iptables = {
      description = "lass-iptables";
      wantedBy = [ "network-pre.target" ];
      before = [ "network-pre.target" ];
      after = [ "systemd-modules-load.service" ];

      path = with pkgs; [
        iptables
      ];

      restartIfChanged = true;

      serviceConfig = {
        Type = "simple";
        RemainAfterExit = true;
        Restart = "always";
        ExecStart = "@${startScript} lass-iptables_start";
      };
    };
  };

  #buildTable :: iptablesVersion -> iptablesAttrSet` -> str
  #todo: differentiate by iptables-version
  buildTables = v: ts:
    let

      declareChain = t: cn:
        #TODO: find out what to do whit these count numbers
        ":${cn} ${t."${cn}".policy} [0:0]";

      buildChain = tn: cn:
        let
          sortedRules = sort (a: b: a.precedence > b.precedence) ts."${tn}"."${cn}".rules;

        in
          #TODO: double check should be unneccessary, refactor!
          if (hasAttr "rules" ts."${tn}"."${cn}") then
            if (ts."${tn}"."${cn}".rules == null) then
              ""
            else
              concatMapStringsSep "\n" (rule: "\n-A ${cn} ${rule}") ([]
                ++ map (buildRule tn cn) sortedRules
              )
          else
            ""
          ;


      buildRule = tn: cn: rule:
        #target validation test:
        assert (elemIsIn rule.target ([ "ACCEPT" "REJECT" "DROP" "QUEUE" "LOG" "RETURN" ] ++ (attrNames ts."${tn}")));

        #predicate validation test:
        #maybe use iptables-test
        #TODO: howto exit with evaluation error by shellscript?
          #apperantly not possible from nix because evalatution wouldn't be deterministic.
        "${rule.predicate} -j ${rule.target}";

      buildTable = tn:
        "*${tn}\n" +
        concatStringsSep "\n" ([]
          ++ map (declareChain ts."${tn}") (attrNames ts."${tn}")
        ) +
        #this looks dirty, find a better way to do this (maybe optionalString)
        concatStringsSep "" ([]
          ++ map (buildChain tn) (attrNames ts."${tn}")
        ) +
        "\nCOMMIT";
    in
      concatStringsSep "\n" ([]
        ++ map buildTable (attrNames ts)
      );

#=====

  rules4 = iptables-version:
    let
      #TODO: find out good defaults.
      tables-defaults = {
        nat.PREROUTING.policy = "ACCEPT";
        nat.INPUT.policy = "ACCEPT";
        nat.OUTPUT.policy = "ACCEPT";
        nat.POSTROUTING.policy = "ACCEPT";
        filter.INPUT.policy = "ACCEPT";
        filter.FORWARD.policy = "ACCEPT";
        filter.OUTPUT.policy = "ACCEPT";

        #if someone specifies any other rules on this chain, the default rules get lost.
        #is this wanted beahiviour or a bug?
        #TODO: implement abstraction of rules
        filter.INPUT.rules = [
          { predicate = "-m conntrack --ctstate RELATED,ESTABLISHED"; target = "ACCEPT"; }
        ];
      };
      tables = tables-defaults // cfg.tables;

    in
      writeText "lass-iptables-rules${toString iptables-version}" ''
        ${buildTables iptables-version tables}
      '';

  startScript = writeScript "lass-iptables_start" ''
    #! /bin/sh
    set -euf
    iptables-restore < ${rules4 4}
    ip6tables-restore < ${rules4 6}
  '';

in
out