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

with utils;
with lib;

let

  cfg = config.networking;
  interfaces = attrValues cfg.interfaces;

  interfaceIps = i:
    i.ipv4.addresses
    ++ optionals cfg.enableIPv6 i.ipv6.addresses;

  interfaceRoutes = i:
    i.ipv4.routes
    ++ optionals cfg.enableIPv6 i.ipv6.routes;

  dhcpStr = useDHCP: if useDHCP == true || useDHCP == null then "yes" else "no";

  slaves =
    concatLists (map (bond: bond.interfaces) (attrValues cfg.bonds))
    ++ concatLists (map (bridge: bridge.interfaces) (attrValues cfg.bridges))
    ++ map (sit: sit.dev) (attrValues cfg.sits)
    ++ map (gre: gre.dev) (attrValues cfg.greTunnels)
    ++ map (vlan: vlan.interface) (attrValues cfg.vlans)
    # add dependency to physical or independently created vswitch member interface
    # TODO: warn the user that any address configured on those interfaces will be useless
    ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);

  defaultGateways = mkMerge (forEach [ cfg.defaultGateway cfg.defaultGateway6 ] (gateway:
    optionalAttrs (gateway != null && gateway.interface != null) {
      networks."40-${gateway.interface}" = {
        matchConfig.Name = gateway.interface;
        routes = [
          ({
            Gateway = gateway.address;
          } // optionalAttrs (gateway.metric != null) {
            Metric = gateway.metric;
          })
        ];
      };
    }
  ));

  genericDhcpNetworks = initrd: mkIf cfg.useDHCP {
    networks."99-ethernet-default-dhcp" = {
      # We want to match physical ethernet interfaces as commonly
      # found on laptops, desktops and servers, to provide an
      # "out-of-the-box" setup that works for common cases.  This
      # heuristic isn't perfect (it could match interfaces with
      # custom names that _happen_ to start with en or eth), but
      # should be good enough to make the common case easy and can
      # be overridden on a case-by-case basis using
      # higher-priority networks or by disabling useDHCP.

      # Type=ether matches veth interfaces as well, and this is
      # more likely to result in interfaces being configured to
      # use DHCP when they shouldn't.

      matchConfig.Name = ["en*" "eth*"];
      DHCP = "yes";
      networkConfig.IPv6PrivacyExtensions = "kernel";
    };
    networks."99-wireless-client-dhcp" = {
      # Like above, but this is much more likely to be correct.
      matchConfig.WLANInterfaceType = "station";
      DHCP = "yes";
      networkConfig.IPv6PrivacyExtensions = "kernel";
      # We also set the route metric to one more than the default
      # of 1024, so that Ethernet is preferred if both are
      # available.
      dhcpV4Config.RouteMetric = 1025;
      ipv6AcceptRAConfig.RouteMetric = 1025;
    };
  };


  interfaceNetworks = mkMerge (forEach interfaces (i: {
    netdevs = mkIf i.virtual ({
      "40-${i.name}" = {
        netdevConfig = {
          Name = i.name;
          Kind = i.virtualType;
        };
        "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
          User = i.virtualOwner;
        };
      };
    });
    networks."40-${i.name}" = {
      name = mkDefault i.name;
      DHCP = mkForce (dhcpStr
        (if i.useDHCP != null then i.useDHCP else (config.networking.useDHCP && i.ipv4.addresses == [ ])));
      address = forEach (interfaceIps i)
        (ip: "${ip.address}/${toString ip.prefixLength}");
      routes = forEach (interfaceRoutes i)
        (route: mkMerge [
          # Most of these route options have not been tested.
          # Please fix or report any mistakes you may find.
          (mkIf (route.address != null && route.prefixLength != null) {
            Destination = "${route.address}/${toString route.prefixLength}";
          })
          (mkIf (route.options ? fastopen_no_cookie) {
            FastOpenNoCookie = route.options.fastopen_no_cookie;
          })
          (mkIf (route.via != null) {
            Gateway = route.via;
          })
          (mkIf (route.type != null) {
            Type = route.type;
          })
          (mkIf (route.options ? onlink) {
            GatewayOnLink = true;
          })
          (mkIf (route.options ? initrwnd) {
            InitialAdvertisedReceiveWindow = route.options.initrwnd;
          })
          (mkIf (route.options ? initcwnd) {
            InitialCongestionWindow = route.options.initcwnd;
          })
          (mkIf (route.options ? pref) {
            IPv6Preference = route.options.pref;
          })
          (mkIf (route.options ? mtu) {
            MTUBytes = route.options.mtu;
          })
          (mkIf (route.options ? metric) {
            Metric = route.options.metric;
          })
          (mkIf (route.options ? src) {
            PreferredSource = route.options.src;
          })
          (mkIf (route.options ? protocol) {
            Protocol = route.options.protocol;
          })
          (mkIf (route.options ? quickack) {
            QuickAck = route.options.quickack;
          })
          (mkIf (route.options ? scope) {
            Scope = route.options.scope;
          })
          (mkIf (route.options ? from) {
            Source = route.options.from;
          })
          (mkIf (route.options ? table) {
            Table = route.options.table;
          })
          (mkIf (route.options ? advmss) {
            TCPAdvertisedMaximumSegmentSize = route.options.advmss;
          })
          (mkIf (route.options ? ttl-propagate) {
            TTLPropagate = route.options.ttl-propagate == "enabled";
          })
        ]);
      networkConfig.IPv6PrivacyExtensions = "kernel";
      linkConfig = optionalAttrs (i.macAddress != null) {
        MACAddress = i.macAddress;
      } // optionalAttrs (i.mtu != null) {
        MTUBytes = toString i.mtu;
      };
    };
  }));

  bridgeNetworks = mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
    netdevs."40-${name}" = {
      netdevConfig = {
        Name = name;
        Kind = "bridge";
      };
    };
    networks = listToAttrs (forEach bridge.interfaces (bi:
      nameValuePair "40-${bi}" {
        DHCP = mkOverride 0 (dhcpStr false);
        networkConfig.Bridge = name;
      }));
  }));

  vlanNetworks = mkMerge (flip mapAttrsToList cfg.vlans (name: vlan: {
    netdevs."40-${name}" = {
      netdevConfig = {
        Name = name;
        Kind = "vlan";
      };
      vlanConfig.Id = vlan.id;
    };
    networks."40-${vlan.interface}" = {
      vlan = [ name ];
    };
  }));

in

{
  config = mkMerge [

  (mkIf config.boot.initrd.network.enable {
    # Note this is if initrd.network.enable, not if
    # initrd.systemd.network.enable. By setting the latter and not the
    # former, the user retains full control over the configuration.
    boot.initrd.systemd.network = mkMerge [
      defaultGateways
      (genericDhcpNetworks true)
      interfaceNetworks
      bridgeNetworks
      vlanNetworks
    ];
    boot.initrd.availableKernelModules =
      optional (cfg.bridges != {}) "bridge" ++
      optional (cfg.vlans != {}) "8021q";
  })

  (mkIf cfg.useNetworkd {

    assertions = [ {
      assertion = cfg.defaultGatewayWindowSize == null;
      message = "networking.defaultGatewayWindowSize is not supported by networkd.";
    } {
      assertion = cfg.defaultGateway != null -> cfg.defaultGateway.interface != null;
      message = "networking.defaultGateway.interface is not optional when using networkd.";
    } {
      assertion = cfg.defaultGateway6 != null -> cfg.defaultGateway6.interface != null;
      message = "networking.defaultGateway6.interface is not optional when using networkd.";
    } ] ++ flip mapAttrsToList cfg.bridges (n: { rstp, ... }: {
      assertion = !rstp;
      message = "networking.bridges.${n}.rstp is not supported by networkd.";
    }) ++ flip mapAttrsToList cfg.fooOverUDP (n: { local, ... }: {
      assertion = local == null;
      message = "networking.fooOverUDP.${n}.local is not supported by networkd.";
    });

    networking.dhcpcd.enable = mkDefault false;

    systemd.network =
      mkMerge [ {
        enable = true;
      }
      defaultGateways
      (genericDhcpNetworks false)
      interfaceNetworks
      bridgeNetworks
      (mkMerge (flip mapAttrsToList cfg.bonds (name: bond: {
        netdevs."40-${name}" = {
          netdevConfig = {
            Name = name;
            Kind = "bond";
          };
          bondConfig = let
            # manual mapping as of 2017-02-03
            # man 5 systemd.netdev [BOND]
            # to https://www.kernel.org/doc/Documentation/networking/bonding.txt
            # driver options.
            driverOptionMapping = let
              trans = f: optName: { valTransform = f; optNames = [optName]; };
              simp  = trans id;
              ms    = trans (v: v + "ms");
              in {
                Mode                       = simp "mode";
                TransmitHashPolicy         = simp "xmit_hash_policy";
                LACPTransmitRate           = simp "lacp_rate";
                MIIMonitorSec              = ms "miimon";
                UpDelaySec                 = ms "updelay";
                DownDelaySec               = ms "downdelay";
                LearnPacketIntervalSec     = simp "lp_interval";
                AdSelect                   = simp "ad_select";
                FailOverMACPolicy          = simp "fail_over_mac";
                ARPValidate                = simp "arp_validate";
                # apparently in ms for this value?! Upstream bug?
                ARPIntervalSec             = simp "arp_interval";
                ARPIPTargets               = simp "arp_ip_target";
                ARPAllTargets              = simp "arp_all_targets";
                PrimaryReselectPolicy      = simp "primary_reselect";
                ResendIGMP                 = simp "resend_igmp";
                PacketsPerSlave            = simp "packets_per_slave";
                GratuitousARP = { valTransform = id;
                                  optNames = [ "num_grat_arp" "num_unsol_na" ]; };
                AllSlavesActive            = simp "all_slaves_active";
                MinLinks                   = simp "min_links";
              };

            do = bond.driverOptions;
            assertNoUnknownOption = let
              knownOptions = flatten (mapAttrsToList (_: kOpts: kOpts.optNames)
                                                     driverOptionMapping);
              # options that apparently donâ€™t exist in the networkd config
              unknownOptions = [ "primary" ];
              assertTrace = bool: msg: if bool then true else builtins.trace msg false;
              in assert all (driverOpt: assertTrace
                               (elem driverOpt (knownOptions ++ unknownOptions))
                               "The bond.driverOption `${driverOpt}` cannot be mapped to the list of known networkd bond options. Please add it to the mapping above the assert or to `unknownOptions` should it not exist in networkd.")
                            (mapAttrsToList (k: _: k) do); "";
            # get those driverOptions that have been set
            filterSystemdOptions = filterAttrs (sysDOpt: kOpts:
                                     any (kOpt: do ? ${kOpt}) kOpts.optNames);
            # build final set of systemd options to bond values
            buildOptionSet = mapAttrs (_: kOpts: with kOpts;
                               # we simply take the first set kernel bond option
                               # (one option has multiple names, which is silly)
                               head (map (optN: valTransform (do.${optN}))
                                 # only map those that exist
                                 (filter (o: do ? ${o}) optNames)));
            in seq assertNoUnknownOption
                   (buildOptionSet (filterSystemdOptions driverOptionMapping));

        };

        networks = listToAttrs (forEach bond.interfaces (bi:
          nameValuePair "40-${bi}" {
            DHCP = mkOverride 0 (dhcpStr false);
            networkConfig.Bond = name;
          }));
      })))
      (mkMerge (flip mapAttrsToList cfg.macvlans (name: macvlan: {
        netdevs."40-${name}" = {
          netdevConfig = {
            Name = name;
            Kind = "macvlan";
          };
          macvlanConfig = optionalAttrs (macvlan.mode != null) { Mode = macvlan.mode; };
        };
        networks."40-${macvlan.interface}" = {
          macvlan = [ name ];
        };
      })))
      (mkMerge (flip mapAttrsToList cfg.fooOverUDP (name: fou: {
        netdevs."40-${name}" = {
          netdevConfig = {
            Name = name;
            Kind = "fou";
          };
          # unfortunately networkd cannot encode dependencies of netdevs on addresses/routes,
          # so we cannot specify Local=, Peer=, PeerPort=. this looks like a missing feature
          # in networkd.
          fooOverUDPConfig = {
            Port = fou.port;
            Encapsulation = if fou.protocol != null then "FooOverUDP" else "GenericUDPEncapsulation";
          } // (optionalAttrs (fou.protocol != null) {
            Protocol = fou.protocol;
          });
        };
      })))
      (mkMerge (flip mapAttrsToList cfg.sits (name: sit: {
        netdevs."40-${name}" = {
          netdevConfig = {
            Name = name;
            Kind = "sit";
          };
          tunnelConfig =
            (optionalAttrs (sit.remote != null) {
              Remote = sit.remote;
            }) // (optionalAttrs (sit.local != null) {
              Local = sit.local;
            }) // (optionalAttrs (sit.ttl != null) {
              TTL = sit.ttl;
            }) // (optionalAttrs (sit.encapsulation != null) (
              {
                FooOverUDP = true;
                Encapsulation =
                  if sit.encapsulation.type == "fou"
                  then "FooOverUDP"
                  else "GenericUDPEncapsulation";
                FOUDestinationPort = sit.encapsulation.port;
              } // (optionalAttrs (sit.encapsulation.sourcePort != null) {
                FOUSourcePort = sit.encapsulation.sourcePort;
              })));
        };
        networks = mkIf (sit.dev != null) {
          "40-${sit.dev}" = {
            tunnel = [ name ];
          };
        };
      })))
      (mkMerge (flip mapAttrsToList cfg.greTunnels (name: gre: {
        netdevs."40-${name}" = {
          netdevConfig = {
            Name = name;
            Kind = gre.type;
          };
          tunnelConfig =
            (optionalAttrs (gre.remote != null) {
              Remote = gre.remote;
            }) // (optionalAttrs (gre.local != null) {
              Local = gre.local;
            }) // (optionalAttrs (gre.ttl != null) {
              TTL = gre.ttl;
            });
        };
        networks = mkIf (gre.dev != null) {
          "40-${gre.dev}" = {
            tunnel = [ name ];
          };
        };
      })))
      vlanNetworks
    ];

    # We need to prefill the slaved devices with networking options
    # This forces the network interface creator to initialize slaves.
    networking.interfaces = listToAttrs (map (i: nameValuePair i { }) slaves);

    systemd.services = let
      # We must escape interfaces due to the systemd interpretation
      subsystemDevice = interface:
        "sys-subsystem-net-devices-${escapeSystemdPath interface}.device";
      # support for creating openvswitch switches
      createVswitchDevice = n: v: nameValuePair "${n}-netdev"
          (let
            deps = map subsystemDevice (attrNames (filterAttrs (_: config: config.type != "internal") v.interfaces));
            ofRules = pkgs.writeText "vswitch-${n}-openFlowRules" v.openFlowRules;
          in
          { description = "Open vSwitch Interface ${n}";
            wantedBy = [ "network.target" (subsystemDevice n) ];
            # and create bridge before systemd-networkd starts because it might create internal interfaces
            before = [ "systemd-networkd.service" ];
            # shutdown the bridge when network is shutdown
            partOf = [ "network.target" ];
            # requires ovs-vswitchd to be alive at all times
            bindsTo = [ "ovs-vswitchd.service" ];
            # start switch after physical interfaces and vswitch daemon
            after = [ "network-pre.target" "ovs-vswitchd.service" ] ++ deps;
            wants = deps; # if one or more interface fails, the switch should continue to run
            serviceConfig.Type = "oneshot";
            serviceConfig.RemainAfterExit = true;
            path = [ pkgs.iproute2 config.virtualisation.vswitch.package ];
            preStart = ''
              echo "Resetting Open vSwitch ${n}..."
              ovs-vsctl --if-exists del-br ${n} -- add-br ${n} \
                        -- set bridge ${n} protocols=${concatStringsSep "," v.supportedOpenFlowVersions}
            '';
            script = ''
              echo "Configuring Open vSwitch ${n}..."
              ovs-vsctl ${concatStrings (mapAttrsToList (name: config: " -- add-port ${n} ${name}" + optionalString (config.vlan != null) " tag=${toString config.vlan}") v.interfaces)} \
                ${concatStrings (mapAttrsToList (name: config: optionalString (config.type != null) " -- set interface ${name} type=${config.type}") v.interfaces)} \
                ${concatMapStrings (x: " -- set-controller ${n} " + x)  v.controllers} \
                ${concatMapStrings (x: " -- " + x) (splitString "\n" v.extraOvsctlCmds)}


              echo "Adding OpenFlow rules for Open vSwitch ${n}..."
              ovs-ofctl --protocols=${v.openFlowVersion} add-flows ${n} ${ofRules}
            '';
            postStop = ''
              echo "Cleaning Open vSwitch ${n}"
              echo "Shutting down internal ${n} interface"
              ip link set dev ${n} down || true
              echo "Deleting flows for ${n}"
              ovs-ofctl --protocols=${v.openFlowVersion} del-flows ${n} || true
              echo "Deleting Open vSwitch ${n}"
              ovs-vsctl --if-exists del-br ${n} || true
            '';
          });
    in mapAttrs' createVswitchDevice cfg.vswitches
      // {
            "network-local-commands" = {
              after = [ "systemd-networkd.service" ];
              bindsTo = [ "systemd-networkd.service" ];
          };
      };
  })

  ];
}
