# services/router.nix — mediaserver acts as the home router
#
# Layout:
#   eno1 = WAN (DHCP from ISP; in phase 1, from the eero still in router mode)
#   eth0 = LAN (static 10.0.0.1/24, serves DHCP to downstream clients)
#
# Services on this box:
#   - systemd-networkd: interface management (replaces NetworkManager here)
#   - nftables: NAT (masquerade out WAN) + firewall (drop WAN inbound except ports.toml)
#   - dnsmasq: DHCP only (port 0 for DNS — AdGuard Home owns :53)
#   - AdGuard Home (already running): DNS for LAN clients
#
# Port forwards live in ../ports.toml so they're easy to edit.

{ config, lib, pkgs, ... }:
let
  portsData = builtins.fromTOML (builtins.readFile ../ports.toml);
  destDefault = portsData.dest_default;

  # Phase-1 transition list; empty now that eero is in bridge mode and
  # eno1 is strictly the ISP-facing WAN.
  trustedLegacyCidrs = [ ];

  legacyTrustRules = lib.concatMapStringsSep "\n            "
    (cidr: ''iifname "eno1" ip saddr ${cidr} accept'')
    trustedLegacyCidrs;

  # Expand "both" into [tcp, udp]; normalise port vs ports; default dest.
  expandForward = entry:
    let
      protos = if entry.protocol == "both" then [ "tcp" "udp" ] else [ entry.protocol ];
      portExpr =
        if entry ? port  then toString entry.port
        else if entry ? ports then builtins.replaceStrings [ "-" ] [ "-" ] entry.ports
        else throw "ports.toml entry '${entry.name}' has neither 'port' nor 'ports'";
      dest = entry.dest or destDefault;
    in
      map (p: { inherit (entry) name; proto = p; port = portExpr; dest = dest; }) protos;

  forwards = lib.concatMap expandForward portsData.forward;

  # nftables accepts port-range literals like "26901-26902" as-is.
  dnatRules = lib.concatMapStringsSep "\n        "
    (f: ''${f.proto} dport ${f.port} dnat to ${f.dest} comment "${f.name}"'')
    forwards;

  # Input-chain accept rules so WAN traffic to forwarded ports reaches the
  # mediaserver. Works in both phases:
  #   phase 1: eero DNATs to 192.168.4.25, arrives on eno1 — matched here.
  #   phase 2: our DNAT rewrites dst to 10.0.0.1 (local), arrives on eno1 — matched here.
  wanPortInputRules = lib.concatMapStringsSep "\n            "
    (f: ''iifname "eno1" ${f.proto} dport ${f.port} accept comment "${f.name}"'')
    forwards;

in
{
  config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") {

    # --- Networking stack: systemd-networkd owns the router NICs ---
    networking.networkmanager.enable = lib.mkForce false;
    networking.useNetworkd = true;
    services.resolved.enable = false;   # AdGuard Home binds :53

    # Disable the scripted firewall — nftables takes over below.
    networking.firewall.enable = false;

    # IP forwarding is required for routing.
    boot.kernel.sysctl = {
      "net.ipv4.ip_forward" = 1;
      "net.ipv4.conf.all.rp_filter" = 1;
      "net.ipv6.conf.all.forwarding" = 0;  # no IPv6 upstream yet
      "net.ipv6.conf.all.disable_ipv6" = 1;
    };

    # --- Interface configuration ---
    systemd.network = {
      enable = true;

      # Pin interface names to MAC addresses so they never swap across boots.
      # Without this, "eth0" is an unpredictable kernel name that depends on
      # device probe order — if the NICs swap, the entire LAN/WAN config breaks.
      links = {
        "10-wan" = {
          matchConfig.MACAddress = "6c:0b:84:0c:4c:59";
          linkConfig.Name = "eno1";
        };
        "20-lan" = {
          matchConfig.MACAddress = "6c:0b:84:0c:4c:58";
          linkConfig.Name = "eth0";
        };
      };

      networks = {
        "10-wan" = {
          matchConfig.Name = "eno1";
          networkConfig = {
            DHCP = "ipv4";
            IPv6AcceptRA = false;
          };
          dhcpV4Config = {
            UseDNS = false;      # don't overwrite resolv.conf from ISP DNS
            UseHostname = false;
          };
        };
        "20-lan" = {
          matchConfig.Name = "eth0";
          networkConfig = {
            Address = "10.0.0.1/24";
            ConfigureWithoutCarrier = true;
            IPv6AcceptRA = false;
          };
          linkConfig.RequiredForOnline = "no";
        };
      };
    };

    # --- nftables: NAT + firewall ---
    networking.nftables = {
      enable = true;
      tables.filter = {
        family = "inet";
        content = ''
          chain input {
            type filter hook input priority 0; policy drop;
            ct state established,related accept
            ct state invalid drop
            iifname "lo" accept
            # LAN is trusted
            iifname "eth0" accept
            # Container bridges reaching host services (e.g. Profilarr → Radarr
            # on 10.0.0.1:7878, Forgejo runner → AdGuard DNS). br-* covers
            # Docker user-defined networks, podman* covers Podman (rootful).
            iifname { "docker0", "br-*", "podman*" } accept
            # Phase 1: also trust the existing eero subnet on eno1 so SSH
            # and AdGuard DNS keep working during the transition.
            ${legacyTrustRules}
            # Accept WAN traffic for ports we publicly expose (ports.toml).
            ${wanPortInputRules}
            # ICMP from anywhere (ping, path-MTU)
            icmp type echo-request accept
            icmpv6 type echo-request accept
          }
          chain forward {
            type filter hook forward priority 0; policy drop;
            ct state established,related accept
            ct state invalid drop
            # LAN → anywhere
            iifname "eth0" accept
            # Containers → anywhere (image pulls, Forgejo runner workflows,
            # etc.). br-* = Docker custom networks, podman* = Podman rootful.
            iifname { "docker0", "br-*", "podman*" } accept
            # WAN → any port-forward target (LAN host or docker container)
            iifname "eno1" ct status dnat accept
          }
          chain output {
            type filter hook output priority 0; policy accept;
          }
        '';
      };
      # Use a distinct table name so we don't share `ip nat` with Docker —
      # Docker manages its own DOCKER/PREROUTING chains in `ip nat`, and
      # NixOS's nftables module rebuilds whichever tables it owns on every
      # activation, which would wipe Docker's rules. Hooks at the same
      # priority across separate tables coexist fine.
      tables.router-nat = {
        family = "ip";
        content = ''
          chain prerouting {
            type nat hook prerouting priority -100; policy accept;
            iifname "eno1" jump port_forwards
          }
          chain port_forwards {
            ${dnatRules}
          }
          chain postrouting {
            type nat hook postrouting priority 100; policy accept;
            oifname "eno1" masquerade
          }
        '';
      };
    };

    # --- DHCP server on the LAN ---
    services.dnsmasq = {
      enable = true;
      settings = {
        interface = "eth0";
        bind-interfaces = true;
        # AdGuard Home owns DNS; dnsmasq only does DHCP.
        port = 0;
        dhcp-range = [ "10.0.0.100,10.0.0.250,12h" ];
        dhcp-option = [
          "option:router,10.0.0.1"
          "option:dns-server,10.0.0.1"
        ];
        # Static reservations — format: "MAC,label,IP"
        dhcp-host = [
          "f0:a7:31:6c:50:4b,camera-bedroom,10.0.0.39"
        ];
        # Helpful: log leases to the journal
        log-dhcp = true;
      };
    };

  };
}
