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

with {
  inherit (lib)
    elemAt
    getExe
    hasAttrByPath
    mkEnableOption
    mkIf
    mkOption
    strings
    types
    ;
};

let
  mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);

  cfg = config.services.pihole-ftl;

  piholeScript = pkgs.writeScriptBin "pihole" ''
    sudo=exec
    if [[ "$USER" != '${cfg.user}' ]]; then
      sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
    fi
    $sudo ${getExe cfg.piholePackage} "$@"
  '';

  settingsFormat = pkgs.formats.toml { };
  settingsFile = settingsFormat.generate "pihole.toml" cfg.settings;
in
{
  options.services.pihole-ftl = {
    enable = mkEnableOption "Pi-hole FTL";

    package = lib.mkPackageOption pkgs "pihole-ftl" { };
    piholePackage = lib.mkPackageOption pkgs "pihole" { };

    privacyLevel = mkOption {
      type = types.numbers.between 0 3;
      description = ''
        Level of detail in generated statistics. 0 enables full statistics, 3
        shows only anonymous statistics.

        See [the documentation](https://docs.pi-hole.net/ftldns/privacylevels).

        Also see services.dnsmasq.settings.log-queries to completely disable
        query logging.
      '';
      default = 0;
      example = 3;
    };

    openFirewallDNS = mkOption {
      type = types.bool;
      default = false;
      description = "Open ports in the firewall for pihole-FTL's DNS server.";
    };

    openFirewallDHCP = mkOption {
      type = types.bool;
      default = false;
      description = "Open ports in the firewall for pihole-FTL's DHCP server.";
    };

    openFirewallWebserver = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Open ports in the firewall for pihole-FTL's webserver, as configured in `settings.webserver.port`.
      '';
    };

    configDirectory = mkOption {
      type = types.path;
      default = "/etc/pihole";
      internal = true;
      readOnly = true;
      description = ''
        Path for pihole configuration.
        pihole does not currently support any path other than /etc/pihole.
      '';
    };

    stateDirectory = mkOption {
      type = types.path;
      default = "/var/lib/pihole";
      description = ''
        Path for pihole state files.
      '';
    };

    logDirectory = mkOption {
      type = types.path;
      default = "/var/log/pihole";
      description = "Path for Pi-hole log files";
    };

    settings = mkOption {
      type = settingsFormat.type;
      description = ''
        Configuration options for pihole.toml.
        See the upstream [documentation](https://docs.pi-hole.net/ftldns/configfile).
      '';
    };

    useDnsmasqConfig = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Import options defined in [](#opt-services.dnsmasq.settings) via
        misc.dnsmasq_lines in Pi-hole's config.
      '';
    };

    macvendorURL = mkOption {
      type = types.str;
      default = "https://ftl.pi-hole.net/macvendor.db";
      description = ''
        URL from which to download the macvendor.db file.
      '';
    };

    pihole = mkOption {
      type = types.package;
      default = piholeScript;
      internal = true;
      description = "Pi-hole admin script";
    };

    lists =
      let
        adlistType = types.submodule {
          options = {
            url = mkOption {
              type = types.str;
              description = "URL of the domain list";
            };
            type = mkOption {
              type = types.enum [
                "allow"
                "block"
              ];
              default = "block";
              description = "Whether domains on this list should be explicitly allowed, or blocked";
            };
            enabled = mkOption {
              type = types.bool;
              default = true;
              description = "Whether this list is enabled";
            };
            description = mkOption {
              type = types.str;
              description = "Description of the list";
              default = "";
            };
          };
        };
      in
      mkOption {
        type = with types; listOf adlistType;
        description = "Deny (or allow) domain lists to use";
        default = [ ];
        example = [
          {
            url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts";
          }
        ];
      };

    user = mkOption {
      type = types.str;
      default = "pihole";
      description = "User to run the service as.";
    };

    group = mkOption {
      type = types.str;
      default = "pihole";
      description = "Group to run the service as.";
    };

    queryLogDeleter = {
      enable = mkEnableOption "Pi-hole FTL DNS query log deleter";

      age = mkOption {
        type = types.int;
        default = 90;
        description = ''
          Delete DNS query logs older than this many days, if
          [](#opt-services.pihole-ftl.queryLogDeleter.enable) is on.
        '';
      };

      interval = mkOption {
        type = types.str;
        default = "weekly";
        description = ''
          How often the query log deleter is run. See systemd.time(7) for more
          information about the format.
        '';
      };
    };

    webserverEnabled = mkOption {
      type = types.bool;
      default = (
        (hasAttrByPath [ "webserver" "port" ] cfg.settings)
        && !builtins.elem cfg.settings.webserver.port [
          ""
          null
        ]
      );
      internal = true;
      description = "Whether the webserver is enabled.";
    };
  };

  config = mkIf cfg.enable {
    assertions = [
      {
        assertion = !config.services.dnsmasq.enable;
        message = "pihole-ftl conflicts with dnsmasq. Please disable one of them.";
      }

      {
        assertion = builtins.length cfg.lists == 0 || cfg.webserverEnabled;
        message = ''
          The Pi-hole webserver must be enabled for lists set in services.pihole-ftl.lists to be automatically loaded on startup via the web API.
          services.pihole-ftl.settings.port must be defined, e.g. by enabling services.pihole-web.enable and defining services.pihole-web.port.
        '';
      }

      {
        assertion =
          builtins.length cfg.lists == 0
          || !(hasAttrByPath [ "webserver" "api" "cli_pw" ] cfg.settings)
          || cfg.settings.webserver.api.cli_pw == true;
        message = ''
          services.pihole-ftl.settings.webserver.api.cli_pw must be true for lists set in services.pihole-ftl.lists to be automatically loaded on startup.
          This enables an ephemeral password used by the pihole command.
        '';
      }
    ];

    services.pihole-ftl.settings = lib.mkMerge [
      # Defaults
      (mkDefaults {
        misc.readOnly = true; # Prevent config changes via API or CLI by default
        webserver.port = ""; # Disable the webserver by default
        misc.privacylevel = cfg.privacyLevel;
      })

      # Move state files to cfg.stateDirectory
      {
        # TODO: Pi-hole currently hardcodes dhcp-leasefile this in its
        # generated dnsmasq.conf, and we can't override it
        misc.dnsmasq_lines = [
          # "dhcp-leasefile=${cfg.stateDirectory}/dhcp.leases"
          # "hostsdir=${cfg.stateDirectory}/hosts"
        ];

        files = {
          database = "${cfg.stateDirectory}/pihole-FTL.db";
          gravity = "${cfg.stateDirectory}/gravity.db";
          macvendor = "${cfg.stateDirectory}/macvendor.db";
          log.ftl = "${cfg.logDirectory}/FTL.log";
          log.dnsmasq = "${cfg.logDirectory}/pihole.log";
          log.webserver = "${cfg.logDirectory}/webserver.log";
        };

        webserver.tls.cert = "${cfg.stateDirectory}/tls.pem";
      }

      (lib.optionalAttrs cfg.useDnsmasqConfig {
        misc.dnsmasq_lines = lib.pipe config.services.dnsmasq.configFile [
          builtins.readFile
          (lib.strings.splitString "\n")
          (builtins.filter (s: s != ""))
        ];
      })
    ];

    systemd.tmpfiles.rules = [
      "d ${cfg.configDirectory} 0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.stateDirectory} 0700 ${cfg.user} ${cfg.group} - -"
      "d ${cfg.logDirectory} 0700 ${cfg.user} ${cfg.group} - -"
    ];

    systemd.services = {
      pihole-ftl =
        let
          setupService = config.systemd.services.pihole-ftl-setup.name;
        in
        {
          description = "Pi-hole FTL";

          after = [ "network.target" ];
          before = [ setupService ];

          wantedBy = [ "multi-user.target" ];
          wants = [ setupService ];

          environment = {
            # Currently unused, but allows the service to be reloaded
            # automatically when the config is changed.
            PIHOLE_CONFIG = settingsFile;

            # pihole is executed by the /actions/gravity API endpoint
            PATH = lib.mkForce (
              lib.makeBinPath [
                cfg.piholePackage
              ]
            );
          };

          serviceConfig = {
            Type = "simple";
            User = cfg.user;
            Group = cfg.group;
            AmbientCapabilities = [
              "CAP_NET_BIND_SERVICE"
              "CAP_NET_RAW"
              "CAP_NET_ADMIN"
              "CAP_SYS_NICE"
              "CAP_IPC_LOCK"
              "CAP_CHOWN"
              "CAP_SYS_TIME"
            ];
            ExecStart = "${getExe cfg.package} no-daemon";
            Restart = "on-failure";
            RestartSec = 1;
            # Hardening
            NoNewPrivileges = true;
            PrivateTmp = true;
            PrivateDevices = true;
            DevicePolicy = "closed";
            ProtectSystem = "strict";
            ProtectHome = "read-only";
            ProtectControlGroups = true;
            ProtectKernelModules = true;
            ProtectKernelTunables = true;
            ReadWritePaths = [
              cfg.configDirectory
              cfg.stateDirectory
              cfg.logDirectory
            ];
            RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
            RestrictNamespaces = true;
            RestrictRealtime = true;
            RestrictSUIDSGID = true;
            MemoryDenyWriteExecute = true;
            LockPersonality = true;
          };
        };

      pihole-ftl-setup = {
        description = "Pi-hole FTL setup";
        enable = builtins.length cfg.lists > 0;

        # Wait for network so lists can be downloaded
        after = [ "network-online.target" ];
        requires = [ "network-online.target" ];
        serviceConfig = {
          Type = "oneshot";
          User = cfg.user;
          Group = cfg.group;

          # Hardening
          NoNewPrivileges = true;
          PrivateTmp = true;
          PrivateDevices = true;
          DevicePolicy = "closed";
          ProtectSystem = "strict";
          ProtectHome = "read-only";
          ProtectControlGroups = true;
          ProtectKernelModules = true;
          ProtectKernelTunables = true;
          ReadWritePaths = [
            cfg.configDirectory
            cfg.stateDirectory
            cfg.logDirectory
          ];
          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
          RestrictNamespaces = true;
          RestrictRealtime = true;
          RestrictSUIDSGID = true;
          MemoryDenyWriteExecute = true;
          LockPersonality = true;
        };
        script = import ./pihole-ftl-setup-script.nix {
          inherit
            cfg
            config
            lib
            pkgs
            ;
        };
      };

      pihole-ftl-log-deleter = mkIf cfg.queryLogDeleter.enable {
        description = "Pi-hole FTL DNS query log deleter";
        serviceConfig = {
          Type = "oneshot";
          User = cfg.user;
          Group = cfg.group;
          # Hardening
          NoNewPrivileges = true;
          PrivateTmp = true;
          PrivateDevices = true;
          DevicePolicy = "closed";
          ProtectSystem = "strict";
          ProtectHome = "read-only";
          ProtectControlGroups = true;
          ProtectKernelModules = true;
          ProtectKernelTunables = true;
          ReadWritePaths = [ cfg.stateDirectory ];
          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
          RestrictNamespaces = true;
          RestrictRealtime = true;
          RestrictSUIDSGID = true;
          MemoryDenyWriteExecute = true;
          LockPersonality = true;
        };
        script =
          let
            days = toString cfg.queryLogDeleter.age;
            database = cfg.settings.files.database;
          in
          ''
            set -euo pipefail

            # Avoid creating an empty database file if it doesn't yet exist
            if [ ! -f "${database}" ]; then
              exit 0;
            fi

            echo "Deleting query logs older than ${days} days"
            ${getExe cfg.package} sqlite3 "${database}" "DELETE FROM query_storage WHERE timestamp <= CAST(strftime('%s', date('now', '-${days} day')) AS INT); select changes() from query_storage limit 1"
          '';
      };
    };

    systemd.timers.pihole-ftl-log-deleter = mkIf cfg.queryLogDeleter.enable {
      description = "Pi-hole FTL DNS query log deleter";
      before = [
        config.systemd.services.pihole-ftl.name
        config.systemd.services.pihole-ftl-setup.name
      ];
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = cfg.queryLogDeleter.interval;
        Unit = "pihole-ftl-log-deleter.service";
      };
    };

    networking.firewall = lib.mkMerge [
      (mkIf cfg.openFirewallDNS {
        allowedUDPPorts = [ 53 ];
        allowedTCPPorts = [ 53 ];
      })

      (mkIf cfg.openFirewallDHCP {
        allowedUDPPorts = [ 67 ];
      })

      (mkIf cfg.openFirewallWebserver {
        allowedTCPPorts = lib.pipe cfg.settings.webserver.port [
          (lib.splitString ",")
          (map (
            port:
            lib.pipe port [
              (builtins.split "[[:alpha:]]+")
              builtins.head
              lib.toInt
            ]
          ))
        ];
      })
    ];

    users.users.${cfg.user} = {
      group = cfg.group;
      isSystemUser = true;
    };

    users.groups.${cfg.group} = { };

    environment.etc."pihole/pihole.toml" = {
      source = settingsFile;
      user = cfg.user;
      group = cfg.group;
      mode = "400";
    };

    environment.systemPackages = [ cfg.pihole ];

    services.logrotate.settings.pihole-ftl = {
      enable = true;
      files = [ "${cfg.logDirectory}/FTL.log" ];
    };
  };

  meta = {
    doc = ./pihole-ftl.md;
    maintainers = with lib.maintainers; [ averyvigolo ];
  };
}
