{
  config,
  pkgs,
  lib,
  ...
}: let
  choose = paths: builtins.head (builtins.filter (p: builtins.pathExists p) paths);
  # Domain and Cloudflare DDNS records configured here. Update this list to add/remove records.
  domain = "prg-radio.org";
  records = ["git" "grafana" "anubis" "wavelog" "pelican" "partdb" "mail" "mailadmin" "@" "test" "minecraft" "wings1"];
  recordsStr = lib.concatStringsSep " " records;
  zoneId = "9fde8d0fa53502f2d1b7e0b1d3765d49";
  envFile = "/home/traefikprg/cloudflare/cloudflare.env";
in {
  options.local = {
    hostname = lib.mkOption {
      type = lib.types.str;
      default = "nixos-default";
      description = "System hostname";
    };
    username = lib.mkOption {
      type = lib.types.str;
      default = "user";
      description = "Primary user username";
    };
    userDescription = lib.mkOption {
      type = lib.types.str;
      default = "NixOS User";
      description = "Primary user description";
    };
    address = lib.mkOption {
      type = lib.types.str;
      default = "10.1.1.100";
      description = "Static IP address";
    };
  };

  imports = [
    (choose [./modules/desktop-manager/sway_greetd_homemanager.nix ../desktop-manager/sway_greetd_homemanager.nix])
    (choose [./modules/local/hostname_username.nix ../local/hostname_username.nix])
    (choose [./modules/local/networking_local.nix ../local/networking_local.nix])
    (choose [./modules/bootloader/seabios-assigned-iso-at-birth.nix ../bootloader/seabios-assigned-iso-at-birth.nix])
    (choose [./modules/lix-default.nix ../lix-default.nix])
    # Optionally: (choose [ ./modules/toolsets/remote_building.nix ../toolsets/remote_building.nix ])
  ];

  config = {
    local.hostname = "nixos-traefik";
    local.username = "traefikprg";
    local.userDescription = "NixOS PRG Traefik Service";
    local.address = "10.1.1.250";

    # Configure Anubis service
    services.anubis = {
      instances = {
        "" = {
          enable = true;
          settings = {
            # Bind to TCP instead of Unix socket for Docker-style integration
            BIND_NETWORK = "tcp";
            BIND = "127.0.0.1:8090";

            # Empty TARGET for redirect mode
            TARGET = " ";

            # Configure redirect domains - ALL domains that should be able to redirect back after challenge
            REDIRECT_DOMAINS = "prg-radio.org,anubis.prg-radio.org,git.prg-radio.org,lgbtq.prg-radio.org,wavelog.prg-radio.org,partdb.prg-radio.org,grafana.prg-radio.org,pelican.prg-radio.org,mail.prg-radio.org,mailadmin.prg-radio.org,minecraft.prg-radio.org";

            # Public URL for Anubis
            PUBLIC_URL = "https://anubis.prg-radio.org";

            # Cookie domain for proper scoping
            COOKIE_DOMAIN = ".prg-radio.org";

            # Difficulty level
            DIFFICULTY = 7;
            #ALGOTIHM = "slow";

            # Optional: serve robots.txt
            SERVE_ROBOTS_TXT = true;

            # Optional: webmaster email for error pages
            WEBMASTER_EMAIL = "dtu.prg@gmail.com";

            # Prevents infinite loop of doom:
            # See: https://github.com/TecharoHQ/anubis/issues/970
            JWT_RESTRICTION_HEADER = "CF-Connecting-IP";

            # Metrics on separate port
            METRICS_BIND_NETWORK = "tcp";
            METRICS_BIND = "127.0.0.1:8091";

            SLOG_LEVEL = "DEBUG";

            COOKIE_SECURE = true;
            COOKIE_SAME_SITE = "None"; # Only if Secure=true and you need cross-site
          };
        };
      };
    };

    services.loki = {
      enable = true;
      configuration = {
        auth_enabled = false;

        server = {
          http_listen_port = 3100;
        };

        ingester = {
          lifecycler = {
            address = "0.0.0.0";
            ring = {
              kvstore = {
                store = "inmemory";
              };
              replication_factor = 1;
            };
            final_sleep = "0s";
          };
          chunk_idle_period = "1h";
          max_chunk_age = "1h";
          chunk_target_size = 1048576;
          chunk_retain_period = "30s";
        };

        schema_config = {
          configs = [
            {
              from = "2020-10-24";
              store = "boltdb-shipper";
              object_store = "filesystem";
              schema = "v11";
              index = {
                prefix = "index_";
                period = "24h";
              };
            }
          ];
        };

        storage_config = {
          boltdb_shipper = {
            active_index_directory = "/var/lib/loki/boltdb-shipper-active";
            cache_location = "/var/lib/loki/boltdb-shipper-cache";
            cache_ttl = "24h";
          };
          filesystem = {
            directory = "/var/lib/loki/chunks";
          };
        };

        limits_config = {
          reject_old_samples = true;
          reject_old_samples_max_age = "168h";
          # Disable structured metadata/OTLP native ingestion until schema is upgraded to v13+ and tsdb index is used.
          allow_structured_metadata = false;
        };

        table_manager = {
          retention_deletes_enabled = false;
          retention_period = "0s";
        };
      };
    };

    services.alloy = {
      enable = true;
    };

    services.prometheus = {
      enable = true;
      port = 9001;

      exporters.node = {
        enabledCollectors = [
          "ethtool"
          "softirqs"
          "systemd"
          "tcpstat"
        ];
        enable = true;
        port = 9002;
      };
      globalConfig.scrape_interval = "5s"; # "1m"
      scrapeConfigs = [
        {
          job_name = "traefik";
          static_configs = [
            {
              targets = ["localhost:8080"];
            }
          ];
        }
      ];
    };

    # See: https://github.com/grafana/alloy/blob/bbe11b0f6a3bd3391108b2d27122c654c535dd8d/docs/sources/set-up/migrate/from-promtail.md
    environment.etc."alloy/config.alloy" = {
      text = ''
        discovery.relabel "metrics_integrations_integrations_traefik" {
        	targets = [{
        		__address__ = "localhost:8080",
        	}]

        	rule {
        		target_label = "instance"
        		replacement  = constants.hostname
        	}
        }

        prometheus.scrape "metrics_integrations_integrations_traefik" {
        	targets    = discovery.relabel.metrics_integrations_integrations_traefik.output
        	job_name   = "integrations/traefik"
        	forward_to = [prometheus.remote_write.default.receiver]
        }

        prometheus.remote_write "default" {
        	endpoint {
         		url = "http://127.0.0.1:9001/api/v1/write"
         	}
        }
      '';
      group = "root";
      mode = "0644";
    };

    services.traefik = {
      enable = true;
      group = "acme";
      staticConfigOptions = {
        metrics = {
          prometheus = {
            addEntryPointsLabels = true;
            addRoutersLabels = true;
            addServicesLabels = true;
            buckets = [0.1 0.3 1.2 5.0];
          };
        };

        entryPoints = {
          web = {
            # Bind on IPv6 as well as IPv4 by using [::]:80
            address = "[::]:80";
            asDefault = true;
            http.redirections.entrypoint = {
              to = "websecure";
              scheme = "https";
            };
          };
          websecure = {
            # Bind on IPv6 as well as IPv4 by using [::]:443
            address = "[::]:443";
            asDefault = true;
            http.tls = {
              domains = [
                {
                  main = "prg-radio.org";
                  sans = [
                    "*.prg-radio.org"
                    "*.pelican.prg-radio.org"
                  ];
                }
              ];
            };
          };
          # Federation entrypoint: external TLS on 8448
          federation = {
            # Bind on IPv6 as well as IPv4 by using [::]:8448
            address = "[::]:8448";
          };
          # TeamSpeak entry points
          teamspeak-voice = {
            # UDP entrypoint bind to IPv6 as well
            address = "[::]:9987/udp";
          };
          teamspeak-squery = {
            address = "[::]:10022/tcp";
          };
          teamspeak-data = {
            address = "[::]:30033/tcp";
          };

          # Mail entrypoints: plain SMTP, implicit SMTPS (465) and IMAPS (993)
          smtp = {
            # plain SMTP (port 25) - TCP passthrough to backend
            address = "[::]:25";
          };
          smtps = {
            # implicit TLS SMTP (port 465) - passthrough to backend
            address = "[::]:465";
          };
          imaps = {
            # implicit TLS IMAP (port 993) - passthrough to backend
            address = "[::]:993";
          };

          # SMPT TCP entrypoint (For Pelican's Wings file service)
          pelican-wings-file = {
            address = "[::]:2022";
          };

          # Minecraft TCP entrypoint (Minecraft Java Edition default port)
          minecraft = {
            address = "[::]:25565";
          };

          # Minecraft VoiceChat UDP entrypoint (VoiceChat mod default port)
          minecraft-voice = {
            address = "[::]:24454/udp";
          };

          # Game ports: Valheim (UDP 2456-2458), OpenRA (TCP 1234), OpenTTD (TCP 3979), Xonotic (UDP 26000)
          valheim-2456 = {address = "[::]:2456/udp";};
          valheim-2457 = {address = "[::]:2457/udp";};
          valheim-2458 = {address = "[::]:2458/udp";};
          openra = {address = "[::]:1234";};
          openttd = {address = "[::]:3979";};
          xonotic = {address = "[::]:26000/udp";};
        };
        log = {
          level = "INFO";
          filePath = "${config.services.traefik.dataDir}/traefik.log";
          format = "json";
        };
        api.dashboard = true;
        api.insecure = true;
      };
      dynamicConfigOptions = {
        tls.certificates = [
          {
            certFile = "/var/lib/acme/prg-radio.org/cert.pem";
            keyFile = "/var/lib/acme/prg-radio.org/key.pem";
          }
        ];

        # Traefik middlewares: Anubis forwardAuth
        http.middlewares = {
          anubisForwardAuth = {
            forwardAuth = {
              # point at the Anubis check endpoint (the subrequest/UI instance)
              address = "http://127.0.0.1:8090/.within.website/x/cmd/anubis/api/check";
            };
          };
          redirect-to-https = {
            redirectScheme = {
              scheme = "https";
            };
          };
          # Middleware to strip the /management prefix when proxying to the mail admin
          mail-strip-management = {
            stripPrefix = {prefixes = ["/management"];};
          };
        };

        http.routers = {
          # Anubis router (for challenge page)
          anubis = {
            rule = "Host(`anubis.prg-radio.org`)";
            service = "anubis";
            entryPoints = ["websecure"];
            tls = {
              certresolver = "acme";
            };
          };

          # Protected service example: Forgejo (attach Anubis forwardAuth middleware)
          forgejo = {
            rule = "Host(`git.prg-radio.org`)";
            service = "forgejo";
            entryPoints = ["websecure"];
            tls = {};
            middlewares = ["anubisForwardAuth"];
          };

          # Pelican panel - exposes the Pelican panel UI
          pelican = {
            rule = "Host(`pelican.prg-radio.org`)";
            service = "pelican";
            entryPoints = ["websecure"];
            tls = {certresolver = "acme";};
          };

          # Wings panel - exposes the Wings
          wings = {
            rule = "Host(`wings1.prg-radio.org`)";
            service = "wings-pelican";
            entryPoints = ["websecure"];
            tls = {certresolver = "acme";};
          };

          # Matrix HTTP router for client requests (Element etc.)
          matrix = {
            rule = "Host(`lgbtq.prg-radio.org`)";
            service = "matrix";
            entryPoints = ["websecure"];
            tls = {};
          };

          # Protected service: Wavelog
          wavelog = {
            rule = "Host(`wavelog.prg-radio.org`)";
            service = "wavelog";
            entryPoints = ["websecure"];
            tls = {};
          };

          # Protected service: PartDB
          partdb = {
            rule = "Host(`partdb.prg-radio.org`)";
            service = "partdb";
            entryPoints = ["websecure"];
            tls = {};
            middlewares = ["anubisForwardAuth"];
          };

          # Protected service: Grafana
          grafana = {
            rule = "Host(`grafana.prg-radio.org`)";
            service = "grafana";
            entryPoints = ["websecure"];
            tls = {};
            middlewares = ["anubisForwardAuth"];
          };

          # Mail HTTP (JMAP / web) - terminate TLS at Traefik and forward to Stalwart JMAP HTTP listener
          mail = {
            rule = "Host(`mail.prg-radio.org`)";
            service = "mail-jmap";
            entryPoints = ["websecure"];
            tls = {certresolver = "acme";};
          };

          # Mail web administration UI (Stalwart management) - exposed under /management
          mail-webadmin = {
            rule = "Host(`mailadmin.prg-radio.org`) && PathPrefix(`/management`)";
            service = "mail-webadmin";
            entryPoints = ["websecure"];
            tls = {certresolver = "acme";};
            # strip the /management prefix before proxying to the backend
            middlewares = ["mail-strip-management"];
          };

          bluemap = {
            rule = "Host(`minecraft.prg-radio.org`)";
            service = "bluemap";
            entryPoints = ["websecure"];
            tls = {};
            # Yeah Bluemap cannot be behind Anubis for some reason
            #middlewares = ["anubisForwardAuth"];
          };
        };

        http.services = {
          # Anubis service (challenge UI / redirect endpoint)
          anubis.loadBalancer = {
            servers = [
              {url = "http://127.0.0.1:8090";}
            ];
          };

          forgejo.loadBalancer = {
            servers = [
              {url = "http://10.1.1.4:30000";}
            ];
          };
          matrix.loadBalancer = {
            servers = [
              {url = "http://10.1.1.248:12244";}
            ];
          };
          wavelog.loadBalancer = {
            servers = [
              {url = "http://10.1.1.249:8086";}
            ];
          };

          partdb.loadBalancer = {
            servers = [
              {url = "http://10.1.1.249:8087";}
            ];
          };

          grafana.loadBalancer = {
            servers = [
              {url = "http://10.1.1.10:3000";}
            ];
          };

          # Pelican panel backend
          pelican.loadBalancer = {
            servers = [
              {url = "http://10.1.1.249:8070";}
            ];
          };

          wings-pelican.loadBalancer = {
            servers = [
              {url = "https://10.1.1.249:8443";}
            ];
          };

          # Mail JMAP HTTP backend
          mail-jmap.loadBalancer = {
            servers = [{url = "http://10.1.1.15:8080";}];
          };

          # Mail webadmin backend (management). Traefik will strip the /management
          # prefix and forward the remainder to the management listener on the mail
          # server which is bound to port 8081.
          mail-webadmin.loadBalancer = {
            servers = [{url = "http://10.1.1.15:8081";}];
          };

          # BlueMap HTTP backend - forward to the Minecraft host webserver on port 8100
          bluemap.loadBalancer = {
            servers = [
              {url = "http://10.1.1.244:8100";}
            ];
          };
        };

        # TCP routing for TeamSpeak
        tcp.routers = {
          teamspeak-squery = {
            rule = "HostSNI(`*`)";
            service = "teamspeak-squery";
            entryPoints = ["teamspeak-squery"];
          };
          teamspeak-data = {
            rule = "HostSNI(`*`)";
            service = "teamspeak-data";
            entryPoints = ["teamspeak-data"];
          };
          # Federation TCP router: incoming on :8448 -> Conduit backend
          matrix-federation = {
            rule = "HostSNI(`*`)";
            service = "matrix-federation";
            entryPoints = ["federation"];
            # Pass TLS through to the backend (Conduit handles TLS on port 12244)
            tls = {
              passthrough = true;
            };
          };
          # Also accept TLS passthrough on port 443 for the Matrix host so
          # other servers that contact :443/_matrix/*
          # will be forwarded to Conduit as well.
          # I guess I had to declare the rule explicitly here again?
          matrix-tls443 = {
            # match by SNI to avoid interfering with other HTTPS sites
            rule = "HostSNI(`lgbtq.prg-radio.org`)";
            service = "matrix-federation";
            entryPoints = ["websecure"];
            tls = {
              passthrough = true;
            };
          };

          # Mail TCP routers: SMTP (25), SMTPS (465 implicit TLS), IMAPS (993 implicit TLS)
          mail-smtp = {
            # catch-all TCP router for port 25
            rule = "HostSNI(`*`)";
            service = "mail-smtp";
            entryPoints = ["smtp"];
          };

          mail-smtps = {
            # SMTPS implicit TLS - passthrough to backend
            rule = "HostSNI(`*`)";
            service = "mail-smtps";
            entryPoints = ["smtps"];
            tls = {passthrough = true;};
          };

          mail-imaps = {
            # IMAPS implicit TLS - passthrough to backend
            rule = "HostSNI(`*`)";
            service = "mail-imaps";
            entryPoints = ["imaps"];
            tls = {passthrough = true;};
          };
          pelican-wings-file = {
            # Use catch-all SNI rule because Minecraft clients don't use TLS/SNI
            rule = "HostSNI(`*`)";
            service = "pelican-wings-file";
            entryPoints = ["pelican-wings-file"];
          };
          # TCP router for Minecraft (plain TCP on 25565)
          minecraft = {
            # Use catch-all SNI rule because Minecraft clients don't use TLS/SNI
            rule = "HostSNI(`*`)";
            service = "minecraft";
            entryPoints = ["minecraft"];
          };
          # TCP game routers
          openra = {
            rule = "HostSNI(`*`)"; # catch-all; replace with stricter rule if desired
            service = "openra";
            entryPoints = ["openra"];
          };
          openttd = {
            rule = "HostSNI(`*`)";
            service = "openttd";
            entryPoints = ["openttd"];
          };
        };

        tcp.services = {
          teamspeak-squery.loadBalancer = {
            servers = [
              {address = "10.1.1.248:10022";}
            ];
          };
          teamspeak-data.loadBalancer = {
            servers = [
              {address = "10.1.1.248:30033";}
            ];
          };
          matrix-federation.loadBalancer = {
            servers = [
              {address = "10.1.1.248:12244";}
            ];
          };

          # Mail TCP services
          mail-smtp.loadBalancer = {
            proxyProtocol = {version = 2;}; # Add this line
            servers = [{address = "10.1.1.15:25";}];
          };

          mail-smtps.loadBalancer = {
            proxyProtocol = {version = 2;}; # Add this line
            servers = [{address = "10.1.1.15:465";}];
          };

          mail-imaps.loadBalancer = {
            proxyProtocol = {version = 2;}; # Add this line
            servers = [{address = "10.1.1.15:993";}];
          };

          # TCP service for SMTP of the Pelican
          pelican-wings-file.loadBalancer = {
            servers = [
              {address = "10.1.1.249:2022";}
            ];
          };

          # TCP service for Minecraft
          minecraft.loadBalancer = {
            servers = [
              {address = "10.1.1.244:25565";}
            ];
          };

          # TCP game services
          openra.loadBalancer = {
            servers = [{address = "10.1.1.249:1234";}];
          };
          openttd.loadBalancer = {
            servers = [{address = "10.1.1.249:3979";}];
          };
        };

        # UDP routing for TeamSpeak voice
        udp.routers = {
          teamspeak-voice = {
            entryPoints = ["teamspeak-voice"];
            service = "teamspeak-voice";
          };

          # UDP router for Minecraft VoiceChat mod
          minecraft-voice = {
            entryPoints = ["minecraft-voice"];
            service = "minecraft-voice";
          };
          # UDP routers for game services
          valheim-2456 = {
            entryPoints = ["valheim-2456"];
            service = "valheim-2456";
          };
          valheim-2457 = {
            entryPoints = ["valheim-2457"];
            service = "valheim-2457";
          };
          valheim-2458 = {
            entryPoints = ["valheim-2458"];
            service = "valheim-2458";
          };
          xonotic = {
            entryPoints = ["xonotic"];
            service = "xonotic";
          };
        };

        udp.services = {
          teamspeak-voice.loadBalancer = {
            servers = [
              {address = "10.1.1.248:9987";}
            ];
          };

          minecraft-voice.loadBalancer = {
            servers = [
              {address = "10.1.1.244:24454";}
            ];
          };

          # UDP game services (Valheim and Xonotic)
          "valheim-2456".loadBalancer = {
            servers = [{address = "10.1.1.249:2456";}];
          };
          "valheim-2457".loadBalancer = {
            servers = [{address = "10.1.1.249:2457";}];
          };
          "valheim-2458".loadBalancer = {
            servers = [{address = "10.1.1.249:2458";}];
          };
          xonotic.loadBalancer = {
            servers = [{address = "10.1.1.249:26000";}];
          };
        };
      };
    };

    security.acme = {
      acceptTerms = true;
      defaults.email = "dtu.prg@gmail.com";
      certs."prg-radio.org" = {
        domain = "*.prg-radio.org";
        extraDomainNames = [
          "*.prg-radio.org"
          "*.pelican.prg-radio.org"
        ];
        group = "acme";
        dnsProvider = "cloudflare";
        environmentFile = "/home/traefikprg/cloudflare/cloudflare.env";
        reloadServices = ["traefik.service"];
      };
    };

    environment.systemPackages = with pkgs; [
      util-linux
    ];

    # PRG Cloudflare DDNS updater - split into a single-run upstream script and a wrapper that loops records
    environment.etc."cloudflare-ddns/update-single.sh" = {
      text = ''
        #!${pkgs.bash}/bin/bash
        ## K0p1-Git cloudflare-ddns-updater (packaged copy)
        ## Upstream: https://github.com/K0p1-Git/cloudflare-ddns-updater
        ## Author: K0p1-Git
        ## License: MIT

        ## change to "bin/sh" when necessary

        auth_email=""                                       # The email used to login 'https://dash.cloudflare.com'
        auth_method="token"                                 # Set to "global" for Global API Key or "token" for Scoped API Token
        auth_key=""                                         # Your API Token or Global API Key
        zone_identifier=""                                  # Can be found in the "Overview" tab of your domain
        record_name=""                                      # Which record you want to be synced
        ttl=3600                                            # Set the DNS TTL (seconds)
        proxy=""                                        # Set the proxy to true or false
        sitename=""                                         # Title of site "Example Site"
        slackchannel=""                                     # Slack Channel #example
        slackuri=""                                         # URI for Slack WebHook "https://hooks.slack.com/services/xxxxx"
        discorduri=""                                       # URI for Discord WebHook "https://discordapp.com/api/webhooks/xxxxx"


        ###########################################
        ## Check if we have a public IP
        ###########################################
        REGEX_IPV4="^(0*(1?[0-9]{1,2}|2([0-4][0-9]|5[0-5]))\\.){3}0*(1?[0-9]{1,2}|2([0-4][0-9]|5[0-5]))$"
        IP_SERVICES=(
          "https://api.ipify.org"
          "https://ipv4.icanhazip.com"
          "https://ipinfo.io/ip"
        )

        # Try all the ip services for a valid IPv4 address
        for service in ''${IP_SERVICES[@]}; do
          echo "Testing service: $service"
          RAW_IP=$(curl -s "$service")
          RAW_IP=$(echo "$RAW_IP" | tr -d '\n' | tr -d '\r' | xargs)
          echo "RAW_IP: '$RAW_IP'"
          if [[ $RAW_IP =~ $REGEX_IPV4 ]]; then
            CURRENT_IP=$BASH_REMATCH
            ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Fetched IP $CURRENT_IP"
            break
          else
            ${pkgs.util-linux}/bin/logger -s "DDNS Updater: IP service $service failed. Raw: '$RAW_IP'"
          fi
        done

        # If a record name was passed as first argument, use it (wrapper now passes it)
        if [ -n "$1" ]; then
          record_name="$1"
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Using record_name from arg: '$record_name'"
        else
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: No record_name arg provided; using record_name env: '$record_name'"
        fi

        # If a zone id was passed as second argument, use it (wrapper now passes it)
        if [ -n "$2" ]; then
          zone_identifier="$2"
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Using zone_identifier from arg: '$zone_identifier'"
        else
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: No zone_identifier arg provided; using zone_identifier env: '$zone_identifier'"
        fi

        # If auth_email/auth_key were passed as the 3rd/4th arguments, use them (wrapper now passes them)
        if [ -n "$3" ]; then
          auth_email="$3"
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Using auth_email from arg: '$auth_email'"
        else
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: No auth_email arg provided; using auth_email env: '$auth_email'"
        fi

        if [ -n "$4" ]; then
          auth_key="$4"
          # avoid printing the key itself to logs; show length instead
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Using auth_key from arg (length=''${#auth_key})"
        else
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: No auth_key arg provided; using auth_key env (length=''${#auth_key})"
        fi

        # Fail fast if we still don't have a zone identifier
        if [ -z "$zone_identifier" ]; then
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: zone_identifier is empty — cannot proceed (invalid zone)."
          exit 2
        fi

        ###########################################
        ## Check and set the proper auth header
        ###########################################
        if [[ "''${auth_method}" == "global" ]]; then
          auth_header="X-Auth-Key:"
        else
          auth_header="Authorization: Bearer"
        fi
        # Output for debugging
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Using auth_method ''${auth_method} with header ''${auth_header}"


        ###########################################
        ## Seek for the A record
        ###########################################

        ${pkgs.util-linux}/bin/logger "DDNS Updater: Check Initiated"
        record=$(${pkgs.curl}/bin/curl -s -X GET "https://api.cloudflare.com/client/v4/zones/''$zone_identifier/dns_records?type=A&name=''$record_name" \
                              -H "X-Auth-Email: ''$auth_email" \
                              -H "''$auth_header ''$auth_key" \
                              -H "Content-Type: application/json")

        # Output for debugging
        CURL_EXIT=$?
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Cloudflare GET exit=$CURL_EXIT response=$record"

        ###########################################
        ## Check if the domain has an A record
        ###########################################
        if [[ $record == *"\\"count\\":0"* ]]; then
          ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Record does not exist, perhaps create one first? (''${CURRENT_IP} for ''${record_name})"
          exit 1
        fi
        # Output for debugging
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Record found for ''${record_name}, checking IP..."


        ###########################################
        ## Get existing IP
        ###########################################
        old_ip=$(echo "$record" | ${pkgs.jq}/bin/jq -r '.result[0].content')
        # Compare if they're the same
        if [[ $CURRENT_IP == $old_ip ]]; then
          ${pkgs.util-linux}/bin/logger "DDNS Updater: IP ($CURRENT_IP) for ''${record_name} has not changed."
          exit 0
        fi
        # Output for debugging
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Current IP ($CURRENT_IP) is different from old IP ($old_ip) for ''${record_name}, updating record..."


        ###########################################
        ## Set the record identifier from result
        ###########################################
        record_identifier=$(echo "$record" | ${pkgs.jq}/bin/jq -r '.result[0].id')
        # Output for debugging
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Record identifier for ''${record_name} is ''${record_identifier}"


        ###########################################
        ## Change the IP@Cloudflare using the API
        ###########################################
        update=$(${pkgs.curl}/bin/curl -sS -o - -w '\nHTTP_STATUS:%{http_code}' -X PATCH "https://api.cloudflare.com/client/v4/zones/$zone_identifier/dns_records/$record_identifier" \
                             -H "X-Auth-Email: $auth_email" \
                             -H "$auth_header $auth_key" \
                             -H "Content-Type: application/json" \
                             --data "{\"type\":\"A\",\"name\":\"$record_name\",\"content\":\"$CURRENT_IP\",\"ttl\":$ttl,\"proxied\":''${proxy}}" 2>&1)
        CURL_EXIT=$?
        ${pkgs.util-linux}/bin/logger -s "DDNS Updater: Cloudflare PATCH exit=$CURL_EXIT response=$update"

        ###########################################
        ## Report the status (simplified payload construction to avoid nested-quote issues)
        ###########################################
        case "$update" in
        *"\\\"success\\\":false"*)
          echo -e "DDNS Updater: ''${CURRENT_IP} ''${record_name} DDNS failed for ''${record_identifier} (''${CURRENT_IP}). DUMPING RESULTS:\n$update" | ${pkgs.util-linux}/bin/logger -s
          if [[ $slackuri != "" ]]; then
            msg="''${sitename} DDNS Update Failed: ''${record_name}: ''${record_identifier} (''${CURRENT_IP})."
            ${pkgs.curl}/bin/curl -L -X POST "''${slackuri}" --data-raw "{\"channel\":\"''${slackchannel}\",\"text\":\"''${msg}\"}"
          fi
          if [[ $discorduri != "" ]]; then
            msg="''${sitename} DDNS Update Failed: ''${record_name}: ''${record_identifier} (''${CURRENT_IP})."
            ${pkgs.curl}/bin/curl -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST --data-raw "{\"content\":\"''${msg}\"}" "''${discorduri}"
          fi
          exit 1;;
        *)
          msg="''${sitename} Updated: ''${record_name}'s new IP Address is ''${CURRENT_IP}"
          ${pkgs.util-linux}/bin/logger "DDNS Updater: ''${CURRENT_IP} ''${record_name} DDNS updated."
          if [[ $slackuri != "" ]]; then
            ${pkgs.curl}/bin/curl -L -X POST "''${slackuri}" --data-raw "{\"channel\":\"''${slackchannel}\",\"text\":\"''${msg}\"}"
          fi
          if [[ $discorduri != "" ]]; then
            ${pkgs.curl}/bin/curl -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST --data-raw "{\"content\":\"''${msg}\"}" "''${discorduri}"
          fi
          exit 0;;
        esac

      '';
      mode = "0755";
      group = "root";
    };

    environment.etc."cloudflare-ddns/update.sh" = {
      text = ''
        #!${pkgs.bash}/bin/bash
        set -euo pipefail

        # Wrapper: source env, map tokens, loop declared records and call the upstream single-run script
        if [ -f "${envFile}" ]; then
          # shellcheck disable=SC1090
          source "${envFile}"
        fi

        # Map env variables from the env file into auth_key/auth_email used by the upstream script
        if [ -n "''${CLOUDFLARE_DNS_API_TOKEN:-}" ]; then
          export auth_key="''${CLOUDFLARE_DNS_API_TOKEN:-}"
        elif [ -n "''${CLOUDFLARE_API_TOKEN:-}" ]; then
          export auth_key="''${CLOUDFLARE_API_TOKEN:-}"
        fi
        if [ -n "''${CLOUDFLARE_USERNAME:-}" ]; then
          export auth_email="''${CLOUDFLARE_USERNAME:-}"
        fi

        # Ensure zone id is exported for the single-run script
        export zone_identifier="${zoneId}"

        # Loop records from the Nix list. "@" maps to the base domain
        for r in ${recordsStr}; do
          if [ "$r" = "@" ]; then
            export record_name="${domain}"
          else
            export record_name="$r.${domain}"
          fi

          # Default to Cloudflare proxy enabled for all records, but explicitly
          # disable the proxy for the Minecraft record so clients connect
          # directly to the origin on port 25565 (Cloudflare does not proxy
          # arbitrary Minecraft TCP without Spectrum).
          if [ "$r" = "minecraft" ]; then
            export proxy="false"
          else
            export proxy="true"
          fi

          # Invoke the single-run script explicitly with the system's bash via env to avoid /bin/bash shebang issues
          ${pkgs.bash}/bin/bash /etc/cloudflare-ddns/update-single.sh "$record_name" "$zone_identifier" "$auth_email" "$auth_key" || true
        done

      '';
      mode = "0755";
      group = "root";
    };

    # Enable Tailscale for remote access to Traefik dashboard and configuration
    services.tailscale.enable = true;

    systemd.services.traefik = {
      after = ["acme-finished-prg-radio.org.target"];
      wants = ["acme-finished-prg-radio.org.target"];
    };

    systemd.services.prg-cloudflare-ddns-updater = {
      description = "PRG Cloudflare DDNS updater";
      after = ["network-online.target"];
      wantedBy = ["multi-user.target"];
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${pkgs.bash}/bin/bash /etc/cloudflare-ddns/update.sh";
        User = "traefikprg";
        # Make sure the service doesn't fail the boot if the script exits non-zero
        Restart = "no";
      };
    };

    systemd.timers.prg-cloudflare-ddns-updater = {
      description = "Run PRG Cloudflare DDNS updater hourly";
      wantedBy = ["timers.target"];
      timerConfig = {
        OnBootSec = "1m"; # run shortly after boot
        OnUnitActiveSec = "1h"; # then every hour
        AccuracySec = "1m";
      };
    };

    # Allow common HTTP/S and control ports,
    networking.firewall.allowedTCPPorts = [80 443 10022 30033 8448 9001 3001 993 465 25 25565 24454 2456 2457 2458 26000 1234 3979 2022];
    networking.firewall.allowedUDPPorts = [80 443 9987 9001 3001 993 465 25 25565 24454 2022 2456 2457 2458 26000 1234 3979 2022];

    system.stateVersion = "25.11";
  };
}
