{ ... }:
{
  _class = "clan.service";
  manifest.name = "clan-core/dyndns";
  manifest.description = "A dynamic DNS service to update domain IPs";
  manifest.categories = [ "Network" ];
  manifest.readme = builtins.readFile ./README.md;

  roles.default = {
    interface =
      { lib, ... }:
      {
        options = {
          server = {
            enable = lib.mkEnableOption "dyndns webserver";
            domain = lib.mkOption {
              type = lib.types.str;
              description = "Domain to serve the webservice on";
            };
            port = lib.mkOption {
              type = lib.types.int;
              default = 54805;
              description = "Port to listen on";
            };
            acmeEmail = lib.mkOption {
              type = lib.types.str;
              description = ''
                Email address for account creation and correspondence from the CA.
                It is recommended to use the same email for all certs to avoid account
                creation limits.
              '';
            };
          };

          period = lib.mkOption {
            type = lib.types.int;
            default = 5;
            description = "Domain update period in minutes";
          };

          settings = lib.mkOption {
            type = lib.types.attrsOf (
              lib.types.submodule (
                { ... }:
                {
                  options = {
                    provider = lib.mkOption {
                      example = "namecheap";
                      type = lib.types.str;
                      description = "The dyndns provider to use";
                    };
                    domain = lib.mkOption {
                      type = lib.types.str;
                      example = "example.com";
                      description = "The top level domain to update.";
                    };
                    secret_field_name = lib.mkOption {
                      example = "api_key";

                      type = lib.types.enum [
                        "password"
                        "token"
                        "api_key"
                        "secret_api_key"
                      ];
                      default = "password";
                      description = "The field name for the secret";
                    };
                    extraSettings = lib.mkOption {
                      type = lib.types.attrsOf lib.types.str;
                      default = { };
                      description = ''
                        Extra settings for the provider.
                        Provider specific settings: https://github.com/qdm12/ddns-updater#configuration
                      '';
                    };
                  };
                }
              )
            );
            default = { };
            description = "Configuration for which domains to update";
          };
        };
      };

    perInstance =
      { settings, ... }:
      {
        nixosModule =
          {
            config,
            lib,
            pkgs,
            ...
          }:
          let
            name = "dyndns";
            cfg = settings;

            # We dedup secrets if they have the same provider + base domain
            secret_id = opt: "${name}-${opt.provider}-${opt.domain}";
            secret_path =
              opt: config.clan.core.vars.generators."${secret_id opt}".files."${secret_id opt}".path;

            # We check that a secret has not been set in extraSettings.
            extraSettingsSafe =
              opt:
              if (builtins.hasAttr opt.secret_field_name opt.extraSettings) then
                throw "Please do not set ${opt.secret_field_name} in extraSettings, it is automatically set by the dyndns module."
              else
                opt.extraSettings;

            service_config = {
              settings = builtins.catAttrs "value" (
                builtins.attrValues (
                  lib.mapAttrs (_: opt: {
                    value =
                      (extraSettingsSafe opt)
                      // {
                        domain = opt.domain;
                        provider = opt.provider;
                      }
                      // {
                        "${opt.secret_field_name}" = secret_id opt;
                      };
                  }) cfg.settings
                )
              );
            };

            secret_generator = _: opt: {
              name = secret_id opt;
              value = {
                share = true;
                migrateFact = "${secret_id opt}";
                prompts.${secret_id opt} = {
                  type = "hidden";
                  persist = true;
                };
              };
            };
          in
          {
            imports = lib.optional cfg.server.enable (
              lib.modules.importApply ./nginx.nix {
                inherit config;
                inherit settings;
                inherit lib;
              }
            );

            clan.core.vars.generators = lib.mkIf (cfg.settings != { }) (
              lib.mapAttrs' secret_generator cfg.settings
            );

            users.groups.${name} = lib.mkIf (cfg.settings != { }) { };
            users.users.${name} = lib.mkIf (cfg.settings != { }) {
              group = name;
              isSystemUser = true;
              description = "User for ${name} service";
              home = "/var/lib/${name}";
              createHome = true;
            };

            services.nginx = lib.mkIf cfg.server.enable {
              virtualHosts = {
                "${cfg.server.domain}" = {
                  forceSSL = true;
                  enableACME = true;
                  locations."/" = {
                    proxyPass = "http://localhost:${toString cfg.server.port}";
                  };
                };
              };
            };

            systemd.services.${name} = lib.mkIf (cfg.settings != { }) {
              path = [ ];
              description = "Dynamic DNS updater";
              after = [ "network.target" ];
              wantedBy = [ "multi-user.target" ];
              environment = {
                MYCONFIG = "${builtins.toJSON service_config}";
                SERVER_ENABLED = if cfg.server.enable then "yes" else "no";
                PERIOD = "${toString cfg.period}m";
                LISTENING_ADDRESS = ":${toString cfg.server.port}";
                GODEBUG = "netdns=go"; # We need to set this untill this has been merged. https://github.com/NixOS/nixpkgs/pull/432758
              };

              serviceConfig =
                let
                  pyscript =
                    pkgs.writers.writePython3Bin "generate_secret_config.py"
                      {
                        libraries = [ ];
                        doCheck = false;
                      }
                      ''
                        import json
                        from pathlib import Path
                        import os

                        cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY"))
                        config_str = os.getenv("MYCONFIG")


                        def get_credential(name):
                            secret_p = cred_dir / name
                            with open(secret_p, 'r') as f:
                                return f.read().strip()


                        config = json.loads(config_str)
                        print(f"Config: {config}")
                        for attrset in config["settings"]:
                            if "password" in attrset:
                                attrset['password'] = get_credential(attrset['password'])
                            elif "token" in attrset:
                                attrset['token'] = get_credential(attrset['token'])
                            elif "secret_api_key" in attrset:
                                attrset['secret_api_key'] = get_credential(attrset['secret_api_key'])
                            elif "api_key" in attrset:
                                attrset['api_key'] = get_credential(attrset['api_key'])
                            else:
                                raise ValueError(f"Missing secret field in {attrset}")

                        # create directory data if it does not exist
                        data_dir = Path('data')
                        data_dir.mkdir(mode=0o770, exist_ok=True)

                        # Create a temporary config file
                        # with appropriate permissions
                        tmp_config_path = data_dir / '.config.json'
                        tmp_config_path.touch(mode=0o660, exist_ok=False)

                        # Write the config with secrets back
                        with open(tmp_config_path, 'w') as f:
                            f.write(json.dumps(config, indent=4))

                        # Move config into place
                        config_path = data_dir / 'config.json'
                        tmp_config_path.rename(config_path)

                        # Set file permissions to read
                        # and write only by the user and group
                        for file in data_dir.iterdir():
                            file.chmod(0o660)
                      '';
                in
                {
                  ExecStartPre = lib.getExe pyscript;
                  ExecStart = lib.getExe pkgs.ddns-updater;
                  LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings;
                  User = name;
                  Group = name;
                  NoNewPrivileges = true;
                  PrivateTmp = true;
                  ProtectSystem = "strict";
                  ReadOnlyPaths = "/";
                  PrivateDevices = "yes";
                  ProtectKernelModules = "yes";
                  ProtectKernelTunables = "yes";
                  WorkingDirectory = "/var/lib/${name}";
                  ReadWritePaths = [
                    "/proc/self"
                    "/var/lib/${name}"
                  ];

                  Restart = "always";
                  RestartSec = 60;
                };
            };
          };
      };
  };
}
