{
  lib,
  pkgs,
  config,
  nodes,
  self,
  ...
}: let
  name = "dns";
  cfg = config.services.skynet."${name}";

  # reads that date to a string (will need to be fixed in 2038)
  current_date = self.lastModified;

  # this gets a list of all domains we have records for
  domains = lib.lists.naturalSort (lib.lists.unique (
    lib.lists.forEach records (x: x.domain)
  ));

  # get the ip's of our servers
  servers = lib.lists.naturalSort (lib.lists.unique (
    lib.lists.forEach (sort_records_a_server records) (x: x.value)
  ));

  domains_owned = [
    # for historic reasons we own this
    "csn.ul.ie"
    # the main one we use now
    "skynet.ie"
    # a backup
    "ulcompsoc.ie"
  ];

  # gets a list of records that match this type
  filter_records_type = records: r_type: builtins.filter (x: x.r_type == r_type) records;
  # Get all the A records that are for servers (base record for them)
  filter_records_a_server = records: builtins.filter (x: builtins.hasAttr "server" x && x.server) (filter_records_type records "A");
  # Every other A record
  filter_records_a = records: builtins.filter (x: builtins.hasAttr "server" x && !x.server) (filter_records_type records "A");

  # These functions are to get the final 3 digits of an IP address so we can use them for reverse pointer
  process_ptr = records: lib.lists.forEach records (x: process_ptr_sub x);
  process_ptr_sub = record: {
    record = builtins.substring 9 3 record.record;
    r_type = "PTR";
    value = record.value;
  };
  ip_ptr_to_int = ip: lib.strings.toInt (builtins.substring 9 3 ip);

  # filter and sort records so we cna group them in the right place later
  sort_records_a_server = records: builtins.sort (a: b: a.record < b.record) (filter_records_a_server records);
  sort_records_a = records: builtins.sort (a: b: (ip_ptr_to_int a.value) < (ip_ptr_to_int b.value)) (filter_records_a records);
  sort_records_cname = records: builtins.sort (a: b: a.value < b.value) (filter_records_type records "CNAME");
  sort_records_ptr = records: builtins.sort (a: b: (lib.strings.toInt a.record) < (lib.strings.toInt b.record)) (process_ptr (filter_records_type records "PTR"));
  sort_records_srv = records: builtins.sort (a: b: a.record < b.record) (filter_records_type records "SRV");

  # a tad overkill but type guarding is useful
  max = x: y:
    assert builtins.isInt x;
    assert builtins.isInt y;
      if x < y
      then y
      else x;

  # get teh max length of a list of strings
  max_len = records: lib.lists.foldr (a: b: (max a b)) 0 (lib.lists.forEach records (record: lib.strings.stringLength record.record));

  # Now that we can get teh max lenth of a list of strings
  # we can pad it out to the max len +1
  # this is so that teh generated file is easier for a human to read
  format_records = records: let
    offset = (max_len records) + 1;
  in
    lib.strings.concatMapStrings (x: "${padString x.record offset} IN ${padString x.r_type 5} ${x.value}\n") records;

  # small function to add spaces until it reaches teh required length
  padString = text: length: fixedWidthString_post length " " text;

  # like lib.strings.fixedWidthString but postfix
  # recursive function to extend a string up to a limit
  fixedWidthString_post = width: filler: str: let
    strw = lib.stringLength str;
    reqWidth = width - (lib.stringLength filler);
  in
    # this is here because we were manually setting teh length, now max_len does that for us
    assert lib.assertMsg (strw <= width) "fixedWidthString_post: requested string length (${toString width}) must not be shorter than actual length (${toString strw})";
      if strw == width
      then str
      else (fixedWidthString_post reqWidth filler str) + filler;

  # base config for domains we own (skynet.ie, csn.ul.ie, ulcompsoc.ie)
  # ";" are comments in this file
  get_config_file = (
    domain: records: ''
      $TTL 60  ; 1 minute
      ; hostmaster@skynet.ie is an email address that recieves stuff related to dns
      @ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
         ; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
         ${toString current_date}
         600        ; Refresh (10 minutes)
         300        ; Retry   (5 minutes)
         604800     ; Expire  (1 week)
         3600       ; Minimum (1 hour)
        )

      ; @ stands for teh root domain so teh A record below is where ${domain} points to
      @ NS  ns1.skynet.ie.
      @ NS  ns2.skynet.ie.

      ; ------------------------------------------
      ; Server Names (A Records)
      ; ------------------------------------------
      ${format_records (sort_records_a_server records)}

      ; ------------------------------------------
      ; A (non server names
      ; ------------------------------------------
      ${format_records (sort_records_a records)}

      ; ------------------------------------------
      ; CNAMES
      ; ------------------------------------------
      ${format_records (sort_records_cname records)}

      ; ------------------------------------------
      ; TXT
      ; ------------------------------------------
      ${format_records (filter_records_type records "TXT")}

      ; ------------------------------------------
      ; MX
      ; ------------------------------------------
      ${format_records (filter_records_type records "MX")}

      ; ------------------------------------------
      ; SRV
      ; ------------------------------------------
      ${format_records (sort_records_srv records)}
    ''
  );

  # https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/4/html/reference_guide/s2-bind-configuration-zone-reverse
  # config for our reverse dns pointers (not properly working)
  get_config_file_rev = (
    domain: ''
      $ORIGIN 64-64.99.1.193.in-addr.arpa.
      $TTL 60  ; 1 minute
      ; hostmaster@skynet.ie is an email address that recieves stuff related to dns
      @ IN SOA ${nameserver}.skynet.ie. hostmaster.skynet.ie. (
         ; Serial (YYYYMMDDCC) this has to be updated for each time the record is updated
         ${toString current_date}
         600        ; Refresh (10 minutes)
         300        ; Retry   (5 minutes)
         604800     ; Expire  (1 week)
         3600       ; Minimum (1 hour)
        )

      @ NS  ns1.skynet.ie.
      @ NS  ns2.skynet.ie.

      ; ------------------------------------------
      ; PTR
      ; ------------------------------------------
      ${format_records (sort_records_ptr records)}
    ''
  );

  # arrays  of teh two nameservers
  nameserver_1 = ["193.1.99.109"];
  nameserver_2 = ["193.1.99.120"];

  primaries = (
    if cfg.server.primary
    then
      # primary servers have no primaries (ones they listen to)
      []
    else if builtins.elem cfg.server.ip nameserver_1
    then nameserver_2
    else nameserver_1
  );

  secondaries = (
    if cfg.server.primary
    then
      if builtins.elem cfg.server.ip nameserver_1
      then nameserver_2
      else nameserver_1
    else []
  );

  # small function to tidy up the spam of the cache networks, would use teh subnet except all external traffic has the ip of teh router
  # now limited explicitly to servers that we are administering
  # See i24-09-30_050 for more information
  create_cache_networks = map (x: "${toString x}/32") servers;

  # standard function to create the etc file, pass in the text and domain and it makes it
  create_entry_etc_sub = domain: text: {
    # Creates /etc/skynet/dns/domain
    "skynet/dns/${domain}" = {
      user = "named";
      group = "named";

      # The UNIX file mode bits
      mode = "0664";

      # content of the file
      text = text;
    };
  };

  # standard function to create the etc file, pass in the text and domain and it makes it
  create_entry_etc = domain: type: let
    domain_records = lib.lists.filter (x: x.domain == domain) records;
  in
    # this is the main type of record that most folks are used to
    if type == "owned"
    then create_entry_etc_sub domain (get_config_file domain domain_records)
    # reverse lookups allow for using an IP to find domains pointing to it
    else if type == "reverse"
    then create_entry_etc_sub domain (get_config_file_rev domain)
    else {};

  create_entry_zone = domain: let
    if_primary_and_owned =
      if cfg.server.primary && (lib.lists.any (item: item == domain) domains_owned)
      then ''
        allow-update { key rfc2136key.skynet.ie.; };
        dnssec-policy default;
        inline-signing yes;
      ''
      else "";
  in {
    "${domain}" = {
      extraConfig = ''
        ${if_primary_and_owned}
        // for bumping the config
        // ${toString current_date}
      '';
      # really wish teh nixos config didnt use master/slave
      master = cfg.server.primary;
      masters = primaries;
      slaves = secondaries;
      # need to write this to a file
      # using the date in it so it will trigger a restart
      file = "/etc/skynet/dns/${domain}";
      # no leading whitespace for first line
    };
  };

  records =
    config.skynet.records
    /*
    Need to "manually" grab it from each server.
    Nix is laxy evalusted so if it does not need to open a file it wont.
    This is to iterate through each server (node) and evaluate the dns records for that server.
    */
    ++ builtins.concatLists (
      lib.attrsets.mapAttrsToList (
        key: value: value.config.services.skynet.dns.records
      )
      nodes
    );

  nameserver =
    if cfg.server.primary
    then "ns1"
    else "ns2";
in {
  imports = [
    ../../config/dns.nix
  ];

  options.services.skynet."${name}" = {
    server = {
      enable = lib.mkEnableOption {
        default = false;
        description = "Skynet DNS server";
        type = lib.types.bool;
      };

      primary = lib.mkOption {
        type = lib.types.bool;
        default = false;
      };

      ip = lib.mkOption {
        type = lib.types.str;
        description = ''
          ip of this server
        '';
      };
    };

    records = lib.mkOption {
      description = "Records, sorted based on therir type";
      type = lib.types.listOf (lib.types.submodule (import ./options-records.nix {
        inherit lib;
      }));
    };
  };

  config = lib.mkIf cfg.server.enable {
    # logging
    services.prometheus.exporters.bind = {
      enable = true;
      openFirewall = true;
    };

    # services.skynet.backup.normal.backups = ["/etc/skynet/dns"];

    # open the firewall for this
    skynet_firewall.forward = [
      "ip daddr ${cfg.server.ip} tcp dport 53 counter packets 0 bytes 0 accept"
      "ip daddr ${cfg.server.ip} udp dport 53 counter packets 0 bytes 0 accept"
    ];

    services.skynet.dns.records = [
      {
        record = nameserver;
        r_type = "A";
        value = config.services.skynet.host.ip;
      }
    ];

    services.bind.zones = lib.attrsets.mergeAttrsList (
      # uses teh domains lsited in teh records
      (lib.lists.forEach domains (domain: (create_entry_zone domain)))
      # we have to do a reverse dns
      ++ [
        (create_entry_zone "64-64.99.1.193.in-addr.arpa")
      ]
    );

    environment.etc = lib.attrsets.mergeAttrsList (
      # uses teh domains lsited in teh records
      (lib.lists.forEach domains (domain: (create_entry_etc domain "owned")))
      # we have to do a reverse dns
      ++ [
        (create_entry_etc "64-64.99.1.193.in-addr.arpa" "reverse")
      ]
    );

    # secrets required
    age.secrets.dns_dnskeys = {
      file = ../../secrets/dns_dnskeys.conf.age;
      owner = "named";
      group = "named";
    };

    # basic but ensure teh dns ports are open
    networking.firewall = {
      allowedTCPPorts = [53];
      allowedUDPPorts = [53];
    };

    services.bind = {
      enable = true;

      ipv4Only = true;

      # need to take a look at https://nixos.org/manual/nixos/unstable/#module-security-acme-config-dns
      extraConfig = ''
        include "/run/agenix/dns_dnskeys";

        statistics-channels {
          inet 127.0.0.1 port 8053 allow { 127.0.0.1; };
        };
      '';

      # piles of no valid RRSIG resolving 'com/DS/IN' errors
      extraOptions = ''
        dnssec-validation yes;
      '';

      # set the upstream dns servers
      # overrides the default dns servers
      forwarders = [
        # Cloudflare
        "1.1.1.1"
        # Google
        "8.8.8.8"
        # Quad9
        "9.9.9.9"
      ];

      cacheNetworks =
        [
          # this server itself
          "127.0.0.0/24"

          # skynet server in the dmz
          "193.1.96.165/32"
          # all of skynet can use this as a resolver
          /*
          Origianl idea, however all external traffic had the ip of the router
          "193.1.99.64/26"

          So to fix this we need to allow smaller ranges? - Didnt work
          Fallback is explisitly listing each ip we have

          Now have a function for it
          */
        ]
        ++ create_cache_networks;
    };

    systemd.services.bind = {
      # deletes teh journal files evey start so it no longer stalls out
      preStart = ''
        rm -vf /etc/skynet/dns/*.jnl
        rm -vf /etc/skynet/dns/*.jbk
      '';
      restartTriggers = [
        "${config.environment.etc."skynet/dns/skynet.ie".source}"
      ];
    };

    # creates a folder in /etc for the dns to use
    users.groups.named = {};

    users.users.named = {
      createHome = true;
      home = "/etc/skynet/dns";
      group = "named";
      # X11 is to ensure the directory can be traversed
      homeMode = "711";
    };
  };
}