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

with lib;

let
  cfg = config.ffnix;
  activeDomains = attrsets.filterAttrs (n: v: v.enable) cfg.domains;

  mkDomain = name: domCfg:
    let
      mkIfName = type:
        if type == "bridge" then "br-${name}" else
        if type == "batman" then "bat-${name}" else
        if type == "fastd" then "fd-${name}" else
        throw "unknown interface type ${type}, coud not generate name";
    in {
      #### NULL-ROUTES ####
      networks."10-lo" = {
        routes = if !domCfg.defaultNullRoute then [] else [
          {
            routeConfig = {
              Destination = "0.0.0.0/0";
              Metric = 200;
              Type = "unreachable";
              Table = domCfg.routingTable;
            };
          }
          {
            routeConfig = {
              Destination = "::/0";
              Metric = 200;
              Type = "unreachable";
              Table = domCfg.routingTable;
            };
          }
        ];
      };

      #### BRIDGE ####
      netdevs."30-${mkIfName "bridge"}".netdevConfig = {
        Name = mkIfName "bridge";
        Kind = "bridge";
      };
      networks."30-${mkIfName "bridge"}" = {
        matchConfig.Name = mkIfName "bridge";
        linkConfig = {
          RequiredForOnline = "no";
          MTUBytes = "${toString domCfg.mtu}";
        };
        address = domCfg.addresses;
        routes = map (prefix: {
          routeConfig = {
            Destination = prefix;
            Scope = "link";
            Table = domCfg.routingTable;
          };
        }) (domCfg.ipv6Prefixes ++ [ domCfg.ipv4Prefix ]);
        routingPolicyRules = [
          {
            routingPolicyRuleConfig = {
              IncomingInterface = mkIfName "bridge";
              Table = domCfg.routingTable;
              Family = "both";
            };
          }
        ] ++ map (prefix: {
          routingPolicyRuleConfig = {
            From = prefix;
            Table = domCfg.routingTable;
          };
        }) (domCfg.ipv6Prefixes ++ [ domCfg.ipv4Prefix ]);
      };

      #### BATMAN ####
      netdevs."30-${mkIfName "batman"}" = mkIf (!cfg.batmanLegacy) {
        netdevConfig = {
          Kind = "batadv";
          Name = mkIfName "batman";
        };
        batmanAdvancedConfig = {
          GatewayMode = "server";
          RoutingAlgorithm = domCfg.batmanAlgorithm;
          OriginatorIntervalSec = 5;
        };
      };
      networks."30-${mkIfName "batman"}" = {
        matchConfig.Name = mkIfName "batman";
        bridge = [ "${mkIfName "bridge"}" ];
      };

      #### FASTD ####
      fdInstances."${mkIfName "fastd"}" = mkIf domCfg.tunnels.fastd.enable ({
        bind = mkDefault [ "any:${toString domCfg.tunnels.fastd.port}" ];
        mtu = domCfg.tunnels.fastd.mtu;
      } // domCfg.tunnels.fastd.extraConfig);
      links."30-${mkIfName "fastd"}" = mkIf domCfg.tunnels.fastd.enable {
        matchConfig.OriginalName = mkIfName "fastd";
        linkConfig.MACAddress = domCfg.tunnels.fastd.interfaceMac;
      };
      networks."30-${mkIfName "fastd"}" = mkIf (domCfg.tunnels.fastd.enable && !cfg.batmanLegacy) {
        matchConfig.Name = mkIfName "fastd";
        networkConfig.BatmanAdvanced = mkIfName "batman";
      };
      services."${mkIfName "batman"}" = mkIf (domCfg.tunnels.fastd.enable && cfg.batmanLegacy) {
        after = [ "fastd-${mkIfName "fastd"}.service" ];
        requiredBy = [ "fastd-${mkIfName "fastd"}.service" ];

        script = ''
          timeout 30 ${pkgs.bash}/bin/sh -c 'while ! ${pkgs.iproute2}/bin/ip link show dev ${mkIfName "fastd"} | grep UNKNOWN ; do sleep 1; done'
          ${pkgs.batctl-legacy}/bin/batctl -m ${mkIfName "batman"} interface add ${mkIfName "fastd"} || true
          ${pkgs.batctl-legacy}/bin/batctl -m ${mkIfName "batman"} gw_mode server || true
        '';

        serviceConfig =  {
          Type = "oneshot";
          RemainAfterExit = true;
        };
      };

      #### KEA / DHCPv4 ####
      keaInterfaces = if (domCfg.dhcpRange == "") then [] else [ "${mkIfName "bridge"}" ];
      keaSubnet4 = mkIf (domCfg.dhcpRange != "") ([ ({
        subnet = domCfg.ipv4Prefix;
        pools = [
          {
            pool = domCfg.dhcpRange;
          }
        ];
        option-data = [
          {
            name = "routers";
            data = head domCfg.addresses;
          }
          {
            name = "domain-name-servers";
            data = head domCfg.addresses;
          }
          {
            name = "domain-name";
            data = domCfg.searchDomain;
          }
        ];
      } // domCfg.dhcpExtraConfig) ]);
    };

    domConfigs = map (key: getAttr key (mapAttrs mkDomain activeDomains)) (attrNames activeDomains);
    mergedConfigs = mapAttrs (name: value: mkMerge value) (attrsets.zipAttrs (map (x: removeAttrs x [ "foo" ]) domConfigs));

in
{
  config = mkIf cfg.enable {
    environment.etc."ffnix.json".source = pkgs.writeText "ffnix.json" (generators.toJSON {} activeDomains);
    systemd.network.netdevs = mergedConfigs.netdevs;
    systemd.network.networks = mergedConfigs.networks;
    systemd.network.links = mergedConfigs.links;

    systemd.services = mergedConfigs.services;

    ffnix.fastd.instances = mergedConfigs.fdInstances;

    services.kea.dhcp4 = mkIf (concatLists mergedConfigs.keaInterfaces.contents != []) {
      enable = true;
      settings = {
        interfaces-config = {
          interfaces = mergedConfigs.keaInterfaces;
        };
        subnet4 = mergedConfigs.keaSubnet4;
      };
    };
  };
}