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 (builtins) toString;
  inherit (lib)
    types
    mkOption
    mkEnableOption
    optionalString
    concatStringsSep
    ;
  inherit (lib.strings) escapeShellArg;
  inherit (lib.attrsets) filterAttrs mapAttrs';
  cfg = config.services.garage;
  fedicfg = config.fediversity.internal.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
        # TODO: endpoin-url should not be hard-coded
        aws --region ${cfg.settings.s3_api.s3_region} --endpoint-url ${fedicfg.api.url} 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,
    }:
    ''
      ## FIXME: Check whether the key exist and skip this step if that is the case. Get rid of this `|| :`
      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 = [ ];
              };
            };
          }
        );
        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 = [ ];
              };
            };
          }
        );
        default = { };
      };
    };
  };

  config = lib.mkIf config.fediversity.enable {
    environment.systemPackages = [
      pkgs.minio-client
      pkgs.awscli
    ];

    networking.firewall.allowedTCPPorts = [ fedicfg.rpc.port ];
    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 = "[::]:${toString fedicfg.rpc.port}";
        rpc_public_addr = "[::1]:${toString fedicfg.rpc.port}";
        s3_api.api_bind_addr = "[::]:${toString fedicfg.api.port}";
        s3_web.bind_addr = "[::]:${toString fedicfg.web.internalPort}";
        s3_web.root_domain = ".${fedicfg.web.rootDomain}";
        index = "index.html";

        s3_api.s3_region = "garage";
        s3_api.root_domain = ".${fedicfg.api.domain}";
      };
    };

    ## Create a proxy from <bucket>.web.garage.<domain> to localhost:3902 for
    ## each bucket that has `website = true`.
    services.nginx.virtualHosts =
      let
        value = {
          forceSSL = true;
          enableACME = true;
          locations."/" = {
            proxyPass = "http://localhost:3902";
            extraConfig = ''
              ## copied from https://garagehq.deuxfleurs.fr/documentation/cookbook/reverse-proxy/
              proxy_set_header Host $host;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              # Disable buffering to a temporary file.
              proxy_max_temp_file_size 0;

              ## NOTE: This page suggests many more options for the object storage
              ## proxy. We should take a look.
              ## https://docs.joinmastodon.org/admin/optional/object-storage-proxy/
            '';
          };
        };
      in
      mapAttrs' (bucket: _: {
        name = fedicfg.web.domainForBucket bucket;
        inherit value;
      }) (filterAttrs (_: { website, ... }: website) cfg.ensureBuckets);

    systemd.services.ensure-garage = {
      after = [ "garage.service" ];
      wantedBy = [ "garage.service" ];
      serviceConfig = {
        Type = "oneshot";
      };
      path = [
        cfg.package
        pkgs.perl
        pkgs.awscli
      ];
      script = ''
        set -xeuo pipefail

        # Give Garage time to start up by waiting until somethings speaks HTTP
        # behind Garage's API URL.
        until ${pkgs.curl}/bin/curl -sio /dev/null ${fedicfg.api.url}; do sleep 1; done

        # 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
        # TODO: if the key already exists, we get an error; hacked with this `|| :` which needs to be removed
        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
      '';
    };
  };
}