{
  config,
  pkgs,
  lib,
  ...
}:
let
  cfg = config.services.ddclient;
  dataDir = "/var/lib/ddclient";
  StateDirectory = baseNameOf dataDir;
  RuntimeDirectory = StateDirectory;

  configFile' = pkgs.writeText "ddclient.conf" ''
    # This file can be used as a template for configFile or is automatically generated by Nix options.
    cache=${dataDir}/ddclient.cache
    foreground=YES
    ${lib.optionalString (cfg.use != "") "use=${cfg.use}"}
    ${lib.optionalString (cfg.use == "" && cfg.usev4 != "") "usev4=${cfg.usev4}"}
    ${lib.optionalString (cfg.use == "" && cfg.usev6 != "") "usev6=${cfg.usev6}"}
    ${lib.optionalString (cfg.username != "") "login=${cfg.username}"}
    ${
      if cfg.protocol == "nsupdate" then
        "password=/run/${RuntimeDirectory}/ddclient.key"
      else if (cfg.passwordFile != null) then
        "password=@password_placeholder@"
      else if (cfg.secretsFile != null) then
        "@secrets_placeholder@"
      else
        ""
    }
    protocol=${cfg.protocol}
    ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
    ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
    ${lib.optionalString (cfg.zone != "") "zone=${cfg.zone}"}
    ssl=${lib.boolToYesNo cfg.ssl}
    wildcard=YES
    quiet=${lib.boolToYesNo cfg.quiet}
    verbose=${lib.boolToYesNo cfg.verbose}
    ${cfg.extraConfig}
    ${lib.concatStringsSep "," cfg.domains}
  '';
  configFile = if (cfg.configFile != null) then cfg.configFile else configFile';

  preStart = ''
    install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
    ${lib.optionalString (cfg.configFile == null) (
      if (cfg.protocol == "nsupdate") then
        ''
          install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
        ''
      else if (cfg.passwordFile != null) then
        ''
          "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
        ''
      else if (cfg.secretsFile != null) then
        ''
          "${pkgs.replace-secret}/bin/replace-secret" "@secrets_placeholder@" "${cfg.secretsFile}" "/run/${RuntimeDirectory}/ddclient.conf"
        ''
      else
        ''
          sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
        ''
    )}
  '';
in
{

  imports = [
    (lib.mkChangedOptionModule [ "services" "ddclient" "domain" ] [ "services" "ddclient" "domains" ] (
      config:
      let
        value = lib.getAttrFromPath [ "services" "ddclient" "domain" ] config;
      in
      lib.optional (value != "") value
    ))
    (lib.mkRemovedOptionModule [ "services" "ddclient" "homeDir" ] "")
    (lib.mkRemovedOptionModule [
      "services"
      "ddclient"
      "password"
    ] "Use services.ddclient.passwordFile instead.")
    (lib.mkRemovedOptionModule [ "services" "ddclient" "ipv6" ] "")
  ];

  ###### interface

  options = {

    services.ddclient = with lib.types; {

      enable = lib.mkOption {
        default = false;
        type = bool;
        description = ''
          Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
        '';
      };

      package = lib.mkOption {
        type = package;
        default = pkgs.ddclient;
        defaultText = lib.literalExpression "pkgs.ddclient";
        description = ''
          The ddclient executable package run by the service.
        '';
      };

      domains = lib.mkOption {
        default = [ "" ];
        type = listOf str;
        description = ''
          Domain name(s) to synchronize.
        '';
      };

      username = lib.mkOption {
        # For `nsupdate` username contains the path to the nsupdate executable
        default = lib.optionalString (
          config.services.ddclient.protocol == "nsupdate"
        ) "${pkgs.bind.dnsutils}/bin/nsupdate";
        defaultText = "";
        type = str;
        description = ''
          User name.
        '';
      };

      passwordFile = lib.mkOption {
        default = null;
        type = nullOr str;
        description = ''
          A file containing the password or a TSIG key in named format when using the nsupdate protocol.
        '';
      };

      secretsFile = lib.mkOption {
        default = null;
        type = nullOr str;
        description = ''
          A file containing the secrets for the dynamic DNS provider.
          This file should contain lines of valid secrets in the format specified by the ddclient documentation.
          If this option is set, it overrides the `passwordFile` option.
        '';
      };

      interval = lib.mkOption {
        default = "10min";
        type = str;
        description = ''
          The interval at which to run the check and update.
          See {command}`man 7 systemd.time` for the format.
        '';
      };

      configFile = lib.mkOption {
        default = null;
        type = nullOr path;
        description = ''
          Path to configuration file.
          When set this overrides the generated configuration from module options.
        '';
        example = "/root/nixos/secrets/ddclient.conf";
      };

      protocol = lib.mkOption {
        default = "dyndns2";
        type = str;
        description = ''
          Protocol to use with dynamic DNS provider (see <https://ddclient.net/protocols.html> ).
        '';
      };

      server = lib.mkOption {
        default = "";
        type = str;
        description = ''
          Server address.
        '';
      };

      ssl = lib.mkOption {
        default = true;
        type = bool;
        description = ''
          Whether to use SSL/TLS to connect to dynamic DNS provider.
        '';
      };

      quiet = lib.mkOption {
        default = false;
        type = bool;
        description = ''
          Print no messages for unnecessary updates.
        '';
      };

      script = lib.mkOption {
        default = "";
        type = str;
        description = ''
          script as required by some providers.
        '';
      };

      use = lib.mkOption {
        default = "";
        type = str;
        description = ''
          Method to determine the IP address to send to the dynamic DNS provider.
        '';
      };
      usev4 = lib.mkOption {
        default = "webv4, webv4=ipify-ipv4";
        type = str;
        description = ''
          Method to determine the IPv4 address to send to the dynamic DNS provider. Only used if `use` is not set.
        '';
      };
      usev6 = lib.mkOption {
        default = "webv6, webv6=ipify-ipv6";
        type = str;
        description = ''
          Method to determine the IPv6 address to send to the dynamic DNS provider. Only used if `use` is not set.
        '';
      };

      verbose = lib.mkOption {
        default = false;
        type = bool;
        description = ''
          Print verbose information.
        '';
      };

      zone = lib.mkOption {
        default = "";
        type = str;
        description = ''
          zone as required by some providers.
        '';
      };

      extraConfig = lib.mkOption {
        default = "";
        type = lines;
        description = ''
          Extra configuration. Contents will be added verbatim to the configuration file.

          ::: {.note}
          `daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
          :::
        '';
      };
    };
  };

  ###### implementation

  config = lib.mkIf config.services.ddclient.enable {
    warnings =
      lib.optional (cfg.use != "")
        "Setting `use` is deprecated, ddclient now supports `usev4` and `usev6` for separate IPv4/IPv6 configuration.";

    assertions = [
      {
        assertion = !((cfg.passwordFile != null) && (cfg.secretsFile != null));
        message = "You cannot use both services.ddclient.passwordFile and services.ddclient.secretsFile at the same time.";
      }
      {
        assertion = (cfg.protocol != "nsupdate") || (cfg.secretsFile == null);
        message = "You cannot use services.ddclient.secretsFile when services.ddclient.protocol is \"nsupdate\". Use services.ddclient.passwordFile instead.";
      }
    ];

    systemd.services.ddclient = {
      description = "Dynamic DNS Client";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      restartTriggers = lib.optional (cfg.configFile != null) cfg.configFile;
      path = lib.optional (
        lib.hasPrefix "if," cfg.use || lib.hasPrefix "ifv4," cfg.usev4 || lib.hasPrefix "ifv6," cfg.usev6
      ) pkgs.iproute2;

      serviceConfig = {
        DynamicUser = true;
        RuntimeDirectoryMode = "0700";
        inherit RuntimeDirectory;
        inherit StateDirectory;
        Type = "oneshot";
        ExecStartPre = [ "!${pkgs.writeShellScript "ddclient-prestart" preStart}" ];
        ExecStart = "${lib.getExe cfg.package} -file /run/${RuntimeDirectory}/ddclient.conf";
      };
    };

    systemd.timers.ddclient = {
      description = "Run ddclient";
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnBootSec = cfg.interval;
        OnUnitInactiveSec = cfg.interval;
      };
    };
  };
}
