{
  lib,
  pkgs,
  config,
  ...
}: let
  inherit (lib.modules) mkIf mkMerge;
  inherit (lib.options) mkOption mkEnableOption;
  inherit (lib.lists) filter;
  inherit (lib) types;
  inherit (builtins) isString listToAttrs attrNames;

  cfg = config.services.secrets;

  secretsLib = import ./secretsLib/lib.nix {
    inherit lib pkgs;
  };
in {
  options = {
    services.secrets = {
      enable = mkEnableOption "secrets";

      debug = mkOption {
        type = types.bool;
        default = false;
      };

      createSecretsDir = mkOption {
        type = types.bool;
        default = true;
      };

      secretsDirUser = mkOption {
        type = types.either types.str types.int;
        default = "root";
      };

      secretsDirGroup = mkOption {
        type = types.either types.str types.int;
        default = "root";
      };

      secretsDir = mkOption {
        type = types.str;
        default = "/secrets";
      };

      vaultLogin = {
        enable = mkOption {
          type = types.bool;
          default = false;
        };
        vaultURL = mkOption {
          type = types.str;
          default = cfg.vaultURL;
        };
        loginUsername = mkOption {
          type = types.str;
        };
        loginPasswordFile = mkOption {
          type = types.nullOr types.path;
          default =
            if cfg.secrets ? "vault_password"
            then cfg.secrets."vault_password".path
            else null;
        };
      };

      autoSecrets = {
        enable = mkEnableOption "autoSecrets";
        affectedSystemdServices = mkOption {
          type = types.listOf (types.either types.str (types.submodule {
            options = {
              name = mkOption {
                type = types.str;
              };
              withPartOf = mkOption {
                type = types.bool;
                default = true;
                description = "add service to auto-secrets's PartOf; this may not be wanted for stuff on a timer such as backups";
              };
            };
            default = {};
          }));
          default = [];
          description = "systemd target names to be restarted with and start after auto-secrets has run, without the .service suffix";
        };
      };

      requiredVaultPaths = mkOption {
        type = types.listOf (types.oneOf [
          types.str
          (types.submodule {
            options = {
              path = mkOption {
                type = types.str;
              };
              capabilities = mkOption {
                type = types.listOf types.str;
                default = ["read" "list"];
              };
            };
          })
        ]);
        default = [];
        description = "default vault paths as API path, not kv2 path";
      };

      vaultURL = mkOption {
        type = types.str;
        default = "https://vault.owo.monster";
        description = "default Vault URL, can be overrided with env variables";
      };

      extraFunctions = mkOption {
        type = types.lines;
        default = "";
        description = "extra bash functions to add to top of script for init";
      };

      extraCheckFunctions = mkOption {
        type = types.lines;
        default = "";
        description = "extra bash functions to add to top of script for check";
      };

      uidMap = mkOption {
        type = types.attrsOf types.int;
        default = listToAttrs (map (
            secretName: let
              secretUserName = cfg.secrets.${secretName}.user;
            in {
              name = "${secretUserName}";
              value = config.users.users.${secretUserName}.uid;
            }
          ) (filter
            (secretName: let
              secret = cfg.secrets.${secretName};
            in
              isString secret.user
              && (
                if config.users.users ? "${secret.user}"
                then true
                else builtins.trace "warning: secrets module could not find a uid mapping for user ${secret.user}" false
              ))
            (attrNames cfg.secrets)));
        description = ''
          optional mapping of users to user IDs
          required for SYSROOT when user isn't available on host
          defaults to getting values from users.users.<name>.uid for all secrets with user set as string
        '';
      };

      gidMap = mkOption {
        type = types.attrsOf types.int;
        default = listToAttrs (map (
            secretName: let
              secretGroupName = cfg.secrets.${secretName}.group;
            in {
              name = "${secretGroupName}";
              value = config.users.groups.${secretGroupName}.gid;
            }
          ) (filter
            (secretName: let
              secret = cfg.secrets.${secretName};
            in
              isString secret.group
              && (
                if config.users.groups ? "${secret.group}"
                then true
                else builtins.trace "warning: secrets module could not find a gid mapping for group ${secret.group}" false
              ))
            (attrNames cfg.secrets)));
        description = ''
          optional mapping of groups to group IDs
          required for SYSROOT when group isn't available on host
          defaults to getting values from users.groups.<group>.gid for all secrets with group set as string
        '';
      };

      packages = mkOption {
        type = types.listOf types.package;
        default = [];
        description = "packages for init script";
      };

      checkPackages = mkOption {
        type = types.listOf types.package;
        default = [];
        description = "packages for checkscript";
      };

      secrets = mkOption {
        type = types.attrsOf (types.submodule ({name, ...}: {
          options = {
            user = mkOption {
              type = types.either types.str types.int;
              default = "root";
            };
            group = mkOption {
              type = types.either types.str types.int;
              default = "root";
            };
            permissions = mkOption {
              type = types.str;
              default = "660";
            };
            path = mkOption {
              type = types.str;
              default = "${cfg.secretsDir}/${name}";
            };

            fetchScript = mkOption {
              type = types.nullOr types.lines;
              default = null;
              description = ''
                script used to fetch secrets, $secretFile is secret.path
              '';
            };

            checkScript = mkOption {
              type = types.nullOr types.lines;
              default = null;
              description = ''
                script used to check contents of secret file, set LOCAL_FAIL to true on failure
              '';
            };

            manual = mkOption {
              type = types.bool;
              default = false;
              description = "should the secret be manually deployed";
            };
          };
        }));
      };
    };
  };

  config =
    {
      assertions = [
        {
          assertion = !(cfg.autoSecrets.enable && !cfg.vaultLogin.enable);
          message = "vaultLogin needs to be enabled and configured for autoSecrets to work";
        }
        {
          assertion = !(cfg.vaultLogin.enable && !cfg.vaultLogin.loginPasswordFile == null);
          message = "loginPasswordFile needs to be set or a secret called vault_password needs to be configured for vaultLogin to work";
        }
      ];
    }
    // (mkMerge [
      (mkIf cfg.enable {
        environment.systemPackages = [
          (secretsLib.mkSecretsInitScript cfg)
          (secretsLib.mkSecretsCheckScript cfg)
        ];
      })

      (mkIf (cfg.enable && cfg.vaultLogin.enable) {
        environment.systemPackages = [
          (secretsLib.mkVaultLoginScript cfg)
        ];
      })

      (mkIf (cfg.enable && cfg.autoSecrets.enable) {
        systemd = let
          # normalise this to make next part easier
          affectedSystemdServices =
            map (unit: let
              name =
                if isString unit
                then unit
                else unit.name;
              withPartOf =
                if isString unit
                then true
                else unit.withPartOf;
            in {
              inherit name withPartOf;
            })
            cfg.autoSecrets.affectedSystemdServices;
        in {
          services =
            (listToAttrs (map (unitConfig: {
                inherit (unitConfig) name;
                value = {
                  after = ["auto-secrets.service"];
                  wants = ["auto-secrets.service"];
                  partOf = mkIf unitConfig.withPartOf ["auto-secrets.service"];
                };
              })
              affectedSystemdServices))
            // {
              auto-secrets.partOf =
                map (unitConfig: unitConfig.name + ".service")
                (filter
                  (unitConfig: unitConfig.withPartOf)
                  affectedSystemdServices);
            };
        };
      })

      (mkIf (cfg.enable && cfg.autoSecrets.enable) {
        systemd.services.auto-secrets = {
          wantedBy = ["multi-user.target"];
          after = ["network.target"];
          path = with pkgs; [bash vault getent];
          script = ''
            ${secretsLib.mkVaultLoginScript cfg}/bin/vault-login
            ${secretsLib.mkSecretsInitScript cfg}/bin/secrets-init
          '';
        };
      })

      (mkIf (cfg.enable && cfg.createSecretsDir) {
        systemd.tmpfiles.rules = [
          "d ${cfg.secretsDir} - ${toString cfg.secretsDirUser} ${toString cfg.secretsDirGroup}"
        ];
      })
    ]);
}
