let
  # generate one using openssl (somehow)
  # XXX: when importing, garage tells you importing is only meant for keys previously generated by garage. is it okay to generate them using openssl? it seems to work fine
  snakeoil_key = {
    id = "GK22a15201acacbd51cd43e327";
    secret = "82b2b4cbef27bf8917b350d5b10a87c92fa9c8b13a415aeeea49726cf335d74e";
  };
in
# TODO: expand to a multi-machine setup
{ config, lib, pkgs, ... }: 
let
  inherit (lib) types mkOption mkEnableOption optionalString concatStringsSep;
  inherit (lib.strings) escapeShellArg;
  cfg = config.services.garage;
  concatMapAttrs = scriptFn: attrset: concatStringsSep "\n" (lib.mapAttrsToList scriptFn attrset);
  ensureBucketScriptFn = bucket: { website, aliases, corsRules }:
    let
      bucketArg = escapeShellArg bucket;
      corsRulesJSON = escapeShellArg (builtins.toJSON {
        CORSRules = [{
          AllowedHeaders = corsRules.allowedHeaders;
          AllowedMethods = corsRules.allowedMethods;
          AllowedOrigins = corsRules.allowedOrigins;
        }];
      });
    in ''
      # garage bucket info tells us if the bucket already exists
      garage bucket info ${bucketArg} || garage bucket create ${bucketArg}

      # TODO: should this --deny the website if `website` is false?
      ${optionalString website ''
        garage bucket website --allow ${bucketArg}
      ''}

      ${concatStringsSep "\n" (map (alias: ''
        garage bucket alias ${bucketArg} ${escapeShellArg alias}
      '') aliases)}

      ${optionalString corsRules.enable ''
        garage bucket allow --read --write --owner ${bucketArg} --key tmp
        aws --endpoint http://s3.garage.localhost:3900 s3api put-bucket-cors --bucket ${bucketArg} --cors-configuration ${corsRulesJSON}
        garage bucket deny --read --write --owner ${bucketArg} --key tmp
      ''}
    '';
  ensureBucketsScript = concatMapAttrs ensureBucketScriptFn cfg.ensureBuckets;
  ensureAccessScriptFn = key: bucket: { read, write, owner }: ''
    garage bucket allow ${optionalString read "--read"} ${optionalString write "--write"} ${optionalString owner "--owner"} \
      ${escapeShellArg bucket} --key ${escapeShellArg key}
  '';
  ensureKeyScriptFn = key: {id, secret, ensureAccess}: ''
    garage key import --yes -n ${escapeShellArg key} ${escapeShellArg id} ${escapeShellArg secret}
    ${concatMapAttrs (ensureAccessScriptFn key) ensureAccess}
  '';
  ensureKeysScript = concatMapAttrs ensureKeyScriptFn cfg.ensureKeys;
in {
  # add in options to ensure creation of buckets and keys
  options = {
    services.garage = {
      ensureBuckets = mkOption {
        type = types.attrsOf (types.submodule {
          options = {
            website = mkOption {
              type = types.bool;
              default = false;
            };
            # I think setting corsRules should allow another website to show images from your bucket
            corsRules = {
              enable = mkEnableOption "CORS Rules";
              allowedHeaders = mkOption {
                type = types.listOf types.str;
                default = [];
              };
              allowedMethods = mkOption {
                type = types.listOf types.str;
                default = [];
              };
              allowedOrigins = mkOption {
                type = types.listOf types.str;
                default = [];
              };
            };
            aliases = mkOption {
              type = types.listOf types.str;
              default = [];
            };
          };
        });
      };
      ensureKeys = mkOption {
        type = types.attrsOf (types.submodule {
          # TODO: these should be managed as secrets, not in the nix store
          options = {
            id = mkOption {
              type = types.str;
            };
            secret = mkOption {
              type = types.str;
            };
            # TODO: assert at least one of these is true
            # NOTE: this currently needs to be done at the top level module
            ensureAccess = mkOption {
              type = types.attrsOf (types.submodule {
                options = {
                  read = mkOption {
                    type = types.bool;
                    default = false;
                  };
                  write = mkOption {
                    type = types.bool;
                    default = false;
                  };
                  owner = mkOption {
                    type = types.bool;
                    default = false;
                  };
                };
              });
              default = [];
            };
          };
        });
      };
    };
  };

  config = {
    virtualisation.diskSize = 2048;
    virtualisation.forwardPorts = [
      {
        from = "host";
        host.port = 3901;
        guest.port = 3901;
      }
      {
        from = "host";
        host.port = 3902;
        guest.port = 3902;
      }
    ];

    environment.systemPackages = [ pkgs.minio-client pkgs.awscli ];

    networking.firewall.allowedTCPPorts = [ 3901 3902 ];
    services.garage = {
      enable = true;
      package = pkgs.garage_0_9;
      settings = {
        replication_mode = "none";
        # TODO: use a secret file
        rpc_secret = "d576c4478cc7d0d94cfc127138cbb82018b0155c037d1c827dfb6c36be5f6625";
        # TODO: why does this have to be set? is there not a sensible default?
        rpc_bind_addr = "[::]:3901";
        rpc_public_addr = "[::1]:3901";
        s3_api.api_bind_addr = "[::]:3900";
        s3_web.bind_addr = "[::]:3902";
        s3_web.root_domain = ".web.garage.localhost";
        index = "index.html";

        s3_api.s3_region = "garage";
        s3_api.root_domain = ".s3.garage.localhost";
      };
    };
    systemd.services.ensure-garage = {
      after = [ "garage.service" ];
      wantedBy = [ "garage.service" ];
      path = [ cfg.package pkgs.perl pkgs.awscli ];
      script = ''
        set -xeuo pipefail
        # give garage time to start up
        sleep 3

        # XXX: this is very sensitive to being a single instance
        # (doing the bare minimum to get garage up and running)
        # also, it's crazy that we have to parse command output like this
        # TODO: talk to garage maintainer about making this nicer to work with in Nix
        # before I do that though, I should figure out how setting it up across multiple machines will work
        GARAGE_ID=$(garage node id 2>/dev/null | perl -ne '/(.*)@.*/ && print $1')
        garage layout assign -z g1 -c 1G $GARAGE_ID
        LAYOUT_VER=$(garage layout show | perl -ne '/Current cluster layout version: (\d*)/ && print $1')
        garage layout apply --version $((LAYOUT_VER + 1))

        # XXX: this is a hack because we want to write to the buckets here but we're not guaranteed any access keys
        # TODO: generate this key here rather than using a well-known key
        garage key import --yes -n tmp ${snakeoil_key.id} ${snakeoil_key.secret}
        export AWS_ACCESS_KEY_ID=${snakeoil_key.id};
        export AWS_SECRET_ACCESS_KEY=${snakeoil_key.secret};

        ${ensureBucketsScript}
        ${ensureKeysScript}

        # garage doesn't like re-adding keys that once existed, so we can't delete / recreate it every time
        # garage key delete ${snakeoil_key.id} --yes
      '';
    };
  };
}