{
  config,
  lib,
  pkgs,
  utils,
  ...
}:

let
  inherit (lib)
    any
    attrValues
    concatStringsSep
    escapeShellArg
    hasInfix
    hasSuffix
    optionalAttrs
    optionals
    literalExpression
    mapAttrs'
    mkEnableOption
    mkOption
    mkPackageOption
    mkIf
    nameValuePair
    types
    ;

  inherit (utils)
    escapeSystemdPath
    ;

  cfg = config.services.gitea-actions-runner;

  settingsFormat = pkgs.formats.yaml { };

  # Check whether any runner instance label requires a container runtime
  # Empty label strings result in the upstream defined defaultLabels, which require docker
  # https://gitea.com/gitea/act_runner/src/tag/v0.1.5/internal/app/cmd/register.go#L93-L98
  hasDockerScheme =
    instance: instance.labels == [ ] || any (label: hasInfix ":docker:" label) instance.labels;
  wantsContainerRuntime = any hasDockerScheme (attrValues cfg.instances);

  hasHostScheme = instance: any (label: hasSuffix ":host" label) instance.labels;

  # provide shorthands for whether container runtimes are enabled
  hasDocker = config.virtualisation.docker.enable;
  hasPodman = config.virtualisation.podman.enable;

  tokenXorTokenFile =
    instance:
    (instance.token == null && instance.tokenFile != null)
    || (instance.token != null && instance.tokenFile == null);
in
{
  meta.maintainers = with lib.maintainers; [
    hexa
  ];

  options.services.gitea-actions-runner = with types; {
    package = mkPackageOption pkgs "gitea-actions-runner" { };

    instances = mkOption {
      default = { };
      description = ''
        Gitea Actions Runner instances.
      '';
      type = attrsOf (submodule {
        options = {
          enable = mkEnableOption "Gitea Actions Runner instance";

          name = mkOption {
            type = str;
            example = literalExpression "config.networking.hostName";
            description = ''
              The name identifying the runner instance towards the Gitea/Forgejo instance.
            '';
          };

          url = mkOption {
            type = str;
            example = "https://forge.example.com";
            description = ''
              Base URL of your Gitea/Forgejo instance.
            '';
          };

          token = mkOption {
            type = nullOr str;
            default = null;
            description = ''
              Plain token to register at the configured Gitea/Forgejo instance.
            '';
          };

          tokenFile = mkOption {
            type = nullOr (either str path);
            default = null;
            description = ''
              Path to an environment file, containing the `TOKEN` environment
              variable, that holds a token to register at the configured
              Gitea/Forgejo instance.
            '';
          };

          labels = mkOption {
            type = listOf str;
            example = literalExpression ''
              [
                # provide a debian base with nodejs for actions
                "debian-latest:docker://node:18-bullseye"
                # fake the ubuntu name, because node provides no ubuntu builds
                "ubuntu-latest:docker://node:18-bullseye"
                # provide native execution on the host
                #"native:host"
              ]
            '';
            description = ''
              Labels used to map jobs to their runtime environment. Changing these
              labels currently requires a new registration token.

              Many common actions require bash, git and nodejs, as well as a filesystem
              that follows the filesystem hierarchy standard.
            '';
          };
          settings = mkOption {
            description = ''
              Configuration for `act_runner daemon`.
              See <https://gitea.com/gitea/act_runner/src/branch/main/internal/pkg/config/config.example.yaml> for an example configuration
            '';

            type = types.submodule {
              freeformType = settingsFormat.type;
            };

            default = { };
          };

          hostPackages = mkOption {
            type = listOf package;
            default = with pkgs; [
              bash
              coreutils
              curl
              gawk
              gitMinimal
              gnused
              nodejs
              wget
            ];
            defaultText = literalExpression ''
              with pkgs; [
                bash
                coreutils
                curl
                gawk
                gitMinimal
                gnused
                nodejs
                wget
              ]
            '';
            description = ''
              List of packages, that are available to actions, when the runner is configured
              with a host execution label.
            '';
          };
        };
      });
    };
  };

  config = mkIf (cfg.instances != { }) {
    assertions = [
      {
        assertion = any tokenXorTokenFile (attrValues cfg.instances);
        message = "Instances of gitea-actions-runner can have `token` or `tokenFile`, not both.";
      }
      {
        assertion = wantsContainerRuntime -> hasDocker || hasPodman;
        message = "Label configuration on gitea-actions-runner instance requires either docker or podman.";
      }
    ];

    systemd.services =
      let
        mkRunnerService =
          name: instance:
          let
            wantsContainerRuntime = hasDockerScheme instance;
            wantsHost = hasHostScheme instance;
            wantsDocker = wantsContainerRuntime && config.virtualisation.docker.enable;
            wantsPodman = wantsContainerRuntime && config.virtualisation.podman.enable;
            configFile = settingsFormat.generate "config.yaml" instance.settings;
          in
          nameValuePair "gitea-runner-${escapeSystemdPath name}" {
            inherit (instance) enable;
            description = "Gitea Actions Runner";
            wants = [ "network-online.target" ];
            after = [
              "network-online.target"
            ]
            ++ optionals wantsDocker [
              "docker.service"
            ]
            ++ optionals wantsPodman [
              "podman.service"
            ];
            wantedBy = [
              "multi-user.target"
            ];
            environment =
              optionalAttrs (instance.token != null) {
                TOKEN = "${instance.token}";
              }
              // optionalAttrs wantsPodman {
                DOCKER_HOST = "unix:///run/podman/podman.sock";
              }
              // {
                HOME = "/var/lib/gitea-runner/${name}";
              };
            path =
              with pkgs;
              [
                coreutils
              ]
              ++ lib.optionals wantsHost instance.hostPackages;
            serviceConfig = {
              DynamicUser = true;
              User = "gitea-runner";
              StateDirectory = "gitea-runner";
              WorkingDirectory = "-/var/lib/gitea-runner/${name}";

              # gitea-runner might fail when gitea is restarted during upgrade.
              Restart = "on-failure";
              RestartSec = 2;

              ExecStartPre = [
                (pkgs.writeShellScript "gitea-register-runner-${name}" ''
                  export INSTANCE_DIR="$STATE_DIRECTORY/${name}"
                  mkdir -vp "$INSTANCE_DIR"
                  cd "$INSTANCE_DIR"

                  # force reregistration on changed labels
                  export LABELS_FILE="$INSTANCE_DIR/.labels"
                  export LABELS_WANTED="$(echo ${escapeShellArg (concatStringsSep "\n" instance.labels)} | sort)"
                  export LABELS_CURRENT="$(cat $LABELS_FILE 2>/dev/null || echo 0)"

                  if [ ! -e "$INSTANCE_DIR/.runner" ] || [ "$LABELS_WANTED" != "$LABELS_CURRENT" ]; then
                    # remove existing registration file, so that changing the labels forces a re-registration
                    rm -v "$INSTANCE_DIR/.runner" || true

                    # perform the registration
                    ${cfg.package}/bin/act_runner register --no-interactive \
                      --instance ${escapeShellArg instance.url} \
                      --token "$TOKEN" \
                      --name ${escapeShellArg instance.name} \
                      --labels ${escapeShellArg (concatStringsSep "," instance.labels)} \
                      --config ${configFile}

                    # and write back the configured labels
                    echo "$LABELS_WANTED" > "$LABELS_FILE"
                  fi

                '')
              ];
              ExecStart = "${cfg.package}/bin/act_runner daemon --config ${configFile}";
              SupplementaryGroups =
                optionals wantsDocker [
                  "docker"
                ]
                ++ optionals wantsPodman [
                  "podman"
                ];
            }
            // optionalAttrs (instance.tokenFile != null) {
              EnvironmentFile = instance.tokenFile;
            };
          };
      in
      mapAttrs' mkRunnerService cfg.instances;
  };
}
