{ config, lib, pkgs, ... }:
with builtins;
with lib;
let
  cfg = config.services.gitlab-runner;
  hasDocker = config.virtualisation.docker.enable;
  hashedServices = mapAttrs'
    (name: service: nameValuePair
      "${name}_${config.networking.hostName}_${
        substring 0 12
        (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
      service)
    cfg.services;
  configPath = "$HOME/.gitlab-runner/config.toml";
  configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
    if (cfg.configFile != null) then ''
      mkdir -p $(dirname ${configPath})
      cp ${cfg.configFile} ${configPath}
      # make config file readable by service
      chown -R --reference=$HOME $(dirname ${configPath})
    '' else ''
      export CONFIG_FILE=${configPath}

      mkdir -p $(dirname ${configPath})

      # remove no longer existing services
      gitlab-runner verify --delete

      # current and desired state
      NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
      REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')

      # difference between current and desired state
      NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
      OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)

      # register new services
      ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
        if echo "$NEW_SERVICES" | grep -xq ${name}; then
          bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
            "set -a && source ${service.registrationConfigFile} &&"
            "gitlab-runner register"
            "--non-interactive"
            "--name ${name}"
            "--executor ${service.executor}"
            "--limit ${toString service.limit}"
            "--request-concurrency ${toString service.requestConcurrency}"
            "--maximum-timeout ${toString service.maximumTimeout}"
          ] ++ service.registrationFlags
            ++ optional (service.buildsDir != null)
            "--builds-dir ${service.buildsDir}"
            ++ optional (service.cloneUrl != null)
            "--clone-url ${service.cloneUrl}"
            ++ optional (service.preCloneScript != null)
            "--pre-clone-script ${service.preCloneScript}"
            ++ optional (service.preBuildScript != null)
            "--pre-build-script ${service.preBuildScript}"
            ++ optional (service.postBuildScript != null)
            "--post-build-script ${service.postBuildScript}"
            ++ optional (service.tagList != [ ])
            "--tag-list ${concatStringsSep "," service.tagList}"
            ++ optional service.runUntagged
            "--run-untagged"
            ++ optional service.protected
            "--access-level ref_protected"
            ++ optional service.debugTraceDisabled
            "--debug-trace-disabled"
            ++ map (e: "--env ${escapeShellArg e}") (mapAttrsToList (name: value: "${name}=${value}") service.environmentVariables)
            ++ optionals (hasPrefix "docker" service.executor) (
              assert (
                assertMsg (service.dockerImage != null)
                  "dockerImage option is required for ${service.executor} executor (${name})");
              [ "--docker-image ${service.dockerImage}" ]
              ++ optional service.dockerDisableCache
              "--docker-disable-cache"
              ++ optional service.dockerPrivileged
              "--docker-privileged"
              ++ map (v: "--docker-volumes ${escapeShellArg v}") service.dockerVolumes
              ++ map (v: "--docker-extra-hosts ${escapeShellArg v}") service.dockerExtraHosts
              ++ map (v: "--docker-allowed-images ${escapeShellArg v}") service.dockerAllowedImages
              ++ map (v: "--docker-allowed-services ${escapeShellArg v}") service.dockerAllowedServices
            )
          ))} && sleep 1 || exit 1
        fi
      '') hashedServices)}

      # unregister old services
      for NAME in $(echo "$OLD_SERVICES")
      do
        [ ! -z "$NAME" ] && gitlab-runner unregister \
          --name "$NAME" && sleep 1
      done

      # update global options
      remarshal --if toml --of json ${configPath} \
        | jq -cM ${escapeShellArg (concatStringsSep " | " [
            ".check_interval = ${toJSON cfg.checkInterval}"
            ".concurrent = ${toJSON cfg.concurrent}"
            ".sentry_dsn = ${toJSON cfg.sentryDSN}"
            ".listen_address = ${toJSON cfg.prometheusListenAddress}"
            ".session_server.listen_address = ${toJSON cfg.sessionServer.listenAddress}"
            ".session_server.advertise_address = ${toJSON cfg.sessionServer.advertiseAddress}"
            ".session_server.session_timeout = ${toJSON cfg.sessionServer.sessionTimeout}"
            "del(.[] | nulls)"
            "del(.session_server[] | nulls)"
          ])} \
        | remarshal --if json --of toml \
        | sponge ${configPath}

      # make config file readable by service
      chown -R --reference=$HOME $(dirname ${configPath})
    '');
  startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
    export CONFIG_FILE=${configPath}
    exec gitlab-runner run --working-directory $HOME
  '';
in
{
  options.services.gitlab-runner = {
    enable = mkEnableOption "Gitlab Runner";
    configFile = mkOption {
      type = types.nullOr types.path;
      default = null;
      description = ''
        Configuration file for gitlab-runner.

        {option}`configFile` takes precedence over {option}`services`.
        {option}`checkInterval` and {option}`concurrent` will be ignored too.

        This option is deprecated, please use {option}`services` instead.
        You can use {option}`registrationConfigFile` and
        {option}`registrationFlags`
        for settings not covered by this module.
      '';
    };
    checkInterval = mkOption {
      type = types.int;
      default = 0;
      example = literalExpression "with lib; (length (attrNames config.services.gitlab-runner.services)) * 3";
      description = ''
        Defines the interval length, in seconds, between new jobs check.
        The default value is 3;
        if set to 0 or lower, the default value will be used.
        See [runner documentation](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#how-check_interval-works) for more information.
      '';
    };
    concurrent = mkOption {
      type = types.int;
      default = 1;
      example = literalExpression "config.nix.maxJobs";
      description = ''
        Limits how many jobs globally can be run concurrently.
        The most upper limit of jobs using all defined runners.
        0 does not mean unlimited.
      '';
    };
    sentryDSN = mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "https://public:private@host:port/1";
      description = ''
        Data Source Name for tracking of all system level errors to Sentry.
      '';
    };
    prometheusListenAddress = mkOption {
      type = types.nullOr types.str;
      default = null;
      example = "localhost:8080";
      description = ''
        Address (&lt;host&gt;:&lt;port&gt;) on which the Prometheus metrics HTTP server
        should be listening.
      '';
    };
    sessionServer = mkOption {
      type = types.submodule {
        options = {
          listenAddress = mkOption {
            type = types.nullOr types.str;
            default = null;
            example = "0.0.0.0:8093";
            description = ''
              An internal URL to be used for the session server.
            '';
          };
          advertiseAddress = mkOption {
            type = types.nullOr types.str;
            default = null;
            example = "runner-host-name.tld:8093";
            description = ''
              The URL that the Runner will expose to GitLab to be used
              to access the session server.
              Fallbacks to {option}`listenAddress` if not defined.
            '';
          };
          sessionTimeout = mkOption {
            type = types.int;
            default = 1800;
            description = ''
              How long in seconds the session can stay active after
              the job completes (which will block the job from finishing).
            '';
          };
        };
      };
      default = { };
      example = literalExpression ''
        {
          listenAddress = "0.0.0.0:8093";
        }
      '';
      description = ''
        The session server allows the user to interact with jobs
        that the Runner is responsible for. A good example of this is the
        [interactive web terminal](https://docs.gitlab.com/ee/ci/interactive_web_terminal/index.html).
      '';
    };
    gracefulTermination = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Finish all remaining jobs before stopping.
        If not set gitlab-runner will stop immediatly without waiting
        for jobs to finish, which will lead to failed builds.
      '';
    };
    gracefulTimeout = mkOption {
      type = types.str;
      default = "infinity";
      example = "5min 20s";
      description = ''
        Time to wait until a graceful shutdown is turned into a forceful one.
      '';
    };
    package = mkOption {
      type = types.package;
      default = pkgs.gitlab-runner;
      defaultText = "pkgs.gitlab-runner";
      example = literalExpression "pkgs.gitlab-runner_1_11";
      description = "Gitlab Runner package to use.";
    };
    extraPackages = mkOption {
      type = types.listOf types.package;
      default = [ ];
      description = ''
        Extra packages to add to PATH for the gitlab-runner process.
      '';
    };
    services = mkOption {
      description = "GitLab Runner services.";
      default = { };
      example = literalExpression ''
        {
          # runner for building in docker via host's nix-daemon
          # nix store will be readable in runner, might be insecure
          nix = {
            # File should contain at least these two variables:
            # `CI_SERVER_URL`
            # `REGISTRATION_TOKEN`
            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
            dockerImage = "alpine";
            dockerVolumes = [
              "/nix/store:/nix/store:ro"
              "/nix/var/nix/db:/nix/var/nix/db:ro"
              "/nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:ro"
            ];
            dockerDisableCache = true;
            preBuildScript = pkgs.writeScript "setup-container" '''
              mkdir -p -m 0755 /nix/var/log/nix/drvs
              mkdir -p -m 0755 /nix/var/nix/gcroots
              mkdir -p -m 0755 /nix/var/nix/profiles
              mkdir -p -m 0755 /nix/var/nix/temproots
              mkdir -p -m 0755 /nix/var/nix/userpool
              mkdir -p -m 1777 /nix/var/nix/gcroots/per-user
              mkdir -p -m 1777 /nix/var/nix/profiles/per-user
              mkdir -p -m 0755 /nix/var/nix/profiles/per-user/root
              mkdir -p -m 0700 "$HOME/.nix-defexpr"

              . ''${pkgs.nix}/etc/profile.d/nix.sh

              ''${pkgs.nix}/bin/nix-env -i ''${concatStringsSep " " (with pkgs; [ nix cacert git openssh ])}

              ''${pkgs.nix}/bin/nix-channel --add https://nixos.org/channels/nixpkgs-unstable
              ''${pkgs.nix}/bin/nix-channel --update nixpkgs
            ''';
            environmentVariables = {
              ENV = "/etc/profile";
              USER = "root";
              NIX_REMOTE = "daemon";
              PATH = "/nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin";
              NIX_SSL_CERT_FILE = "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt";
            };
            tagList = [ "nix" ];
          };
          # runner for building docker images
          docker-images = {
            # File should contain at least these two variables:
            # `CI_SERVER_URL`
            # `REGISTRATION_TOKEN`
            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
            dockerImage = "docker:stable";
            dockerVolumes = [
              "/var/run/docker.sock:/var/run/docker.sock"
            ];
            tagList = [ "docker-images" ];
          };
          # runner for executing stuff on host system (very insecure!)
          # make sure to add required packages (including git!)
          # to `environment.systemPackages`
          shell = {
            # File should contain at least these two variables:
            # `CI_SERVER_URL`
            # `REGISTRATION_TOKEN`
            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
            executor = "shell";
            tagList = [ "shell" ];
          };
          # runner for everything else
          default = {
            # File should contain at least these two variables:
            # `CI_SERVER_URL`
            # `REGISTRATION_TOKEN`
            registrationConfigFile = "/run/secrets/gitlab-runner-registration";
            dockerImage = "debian:stable";
          };
        }
      '';
      type = types.attrsOf (types.submodule {
        options = {
          registrationConfigFile = mkOption {
            type = types.path;
            description = ''
              Absolute path to a file with environment variables
              used for gitlab-runner registration.
              A list of all supported environment variables can be found in
              `gitlab-runner register --help`.

              Ones that you probably want to set is

              `CI_SERVER_URL=<CI server URL>`

              `REGISTRATION_TOKEN=<registration secret>`
            '';
          };
          registrationFlags = mkOption {
            type = types.listOf types.str;
            default = [ ];
            example = [ "--docker-helper-image my/gitlab-runner-helper" ];
            description = ''
              Extra command-line flags passed to
              `gitlab-runner register`.
              Execute `gitlab-runner register --help`
              for a list of supported flags.
            '';
          };
          environmentVariables = mkOption {
            type = types.attrsOf types.str;
            default = { };
            example = { NAME = "value"; };
            description = ''
              Custom environment variables injected to build environment.
              For secrets you can use {option}`registrationConfigFile`
              with `RUNNER_ENV` variable set.
            '';
          };
          executor = mkOption {
            type = types.str;
            default = "docker";
            description = ''
              Select executor, eg. shell, docker, etc.
              See [runner documentation](https://docs.gitlab.com/runner/executors/README.html) for more information.
            '';
          };
          buildsDir = mkOption {
            type = types.nullOr types.path;
            default = null;
            example = "/var/lib/gitlab-runner/builds";
            description = ''
              Absolute path to a directory where builds will be stored
              in context of selected executor (Locally, Docker, SSH).
            '';
          };
          cloneUrl = mkOption {
            type = types.nullOr types.str;
            default = null;
            example = "http://gitlab.example.local";
            description = ''
              Overwrite the URL for the GitLab instance. Used if the Runner can’t connect to GitLab on the URL GitLab exposes itself.
            '';
          };
          dockerImage = mkOption {
            type = types.nullOr types.str;
            default = null;
            description = ''
              Docker image to be used.
            '';
          };
          dockerVolumes = mkOption {
            type = types.listOf types.str;
            default = [ ];
            example = [ "/var/run/docker.sock:/var/run/docker.sock" ];
            description = ''
              Bind-mount a volume and create it
              if it doesn't exist prior to mounting.
            '';
          };
          dockerDisableCache = mkOption {
            type = types.bool;
            default = false;
            description = ''
              Disable all container caching.
            '';
          };
          dockerPrivileged = mkOption {
            type = types.bool;
            default = false;
            description = ''
              Give extended privileges to container.
            '';
          };
          dockerExtraHosts = mkOption {
            type = types.listOf types.str;
            default = [ ];
            example = [ "other-host:127.0.0.1" ];
            description = ''
              Add a custom host-to-IP mapping.
            '';
          };
          dockerAllowedImages = mkOption {
            type = types.listOf types.str;
            default = [ ];
            example = [ "ruby:*" "python:*" "php:*" "my.registry.tld:5000/*:*" ];
            description = ''
              Whitelist allowed images.
            '';
          };
          dockerAllowedServices = mkOption {
            type = types.listOf types.str;
            default = [ ];
            example = [ "postgres:9" "redis:*" "mysql:*" ];
            description = ''
              Whitelist allowed services.
            '';
          };
          preCloneScript = mkOption {
            type = types.nullOr types.path;
            default = null;
            description = ''
              Runner-specific command script executed before code is pulled.
            '';
          };
          preBuildScript = mkOption {
            type = types.nullOr types.path;
            default = null;
            description = ''
              Runner-specific command script executed after code is pulled,
              just before build executes.
            '';
          };
          postBuildScript = mkOption {
            type = types.nullOr types.path;
            default = null;
            description = ''
              Runner-specific command script executed after code is pulled
              and just after build executes.
            '';
          };
          tagList = mkOption {
            type = types.listOf types.str;
            default = [ ];
            description = ''
              Tag list.
            '';
          };
          runUntagged = mkOption {
            type = types.bool;
            default = false;
            description = ''
              Register to run untagged builds; defaults to
              `true` when {option}`tagList` is empty.
            '';
          };
          limit = mkOption {
            type = types.int;
            default = 0;
            description = ''
              Limit how many jobs can be handled concurrently by this service.
              0 (default) simply means don't limit.
            '';
          };
          requestConcurrency = mkOption {
            type = types.int;
            default = 0;
            description = ''
              Limit number of concurrent requests for new jobs from GitLab.
            '';
          };
          maximumTimeout = mkOption {
            type = types.int;
            default = 0;
            description = ''
              What is the maximum timeout (in seconds) that will be set for
              job when using this Runner. 0 (default) simply means don't limit.
            '';
          };
          protected = mkOption {
            type = types.bool;
            default = false;
            description = ''
              When set to true Runner will only run on pipelines
              triggered on protected branches.
            '';
          };
          debugTraceDisabled = mkOption {
            type = types.bool;
            default = false;
            description = ''
              When set to true Runner will disable the possibility of
              using the `CI_DEBUG_TRACE` feature.
            '';
          };
        };
      });
    };
  };
  config = mkIf cfg.enable {

    users.users.gitlab-runner =
      { name = "gitlab-runner";
        uid = mkDefault 532;
        # gid = mkDefault config.users.groups.gitlab-runner.gid;
        home = mkDefault "/var/lib/gitlab-runner";
        shell = "/bin/bash";
        description = "Gitlab agent user";
      };
    users.groups.gitlab-runner =
      { name = "gitlab-runner";
        gid = mkDefault 532;
        description = "Gitlab agent user group";
      };


    # system.activationScripts.preActivation.text = let user = config.users.users.gitlab-runner; in ''
    #  mkdir -p '${user.home}'
    #  chown ${toString user.uid}:${toString user.gid} '${user.home}'
    #'';


    warnings = optional (cfg.configFile != null) "services.gitlab-runner.`configFile` is deprecated, please use services.gitlab-runner.`services`.";
    environment.systemPackages = [ cfg.package ];

    launchd.daemons.gitlab-runner = {
      environment = { #config.networking.proxy.envVars // {
        HOME = "${config.users.users.gitlab-runner.home}";
        NIX_REMOTE = "daemon";
        NIX_SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
      };
      path = with pkgs; [
        bash
        gawk
        jq
        moreutils
        remarshal
        # util-linux
        cfg.package
        coreutils
        gnugrep
        gnused
      ] ++ cfg.extraPackages;

        script = ''
            ${configureScript}/bin/gitlab-runner-configure && ${startScript}/bin/gitlab-runner-start
        '';

        serviceConfig = {
            ProcessType = "Interactive";
            ThrottleInterval = 30;

          # StandardOutPath = "/var/lib/gitlab-runner/out.log";
          # StandardErrorPath = "/var/lib/gitlab-runner/err.log";
          # The combination of KeepAlive.NetworkState and WatchPaths
          # will ensure that buildkite-agent is started on boot, but
          # after networking is available (so the hostname is
          # correct).
          RunAtLoad = true;
        #   KeepAlive.NetworkState = true;
          WatchPaths = [
            "/etc/resolv.conf"
            "/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist"
          ];

          GroupName = "gitlab-runner";
          UserName  = "gitlab-runner";
          WorkingDirectory = config.users.users.gitlab-runner.home;
        };

    };
    # systemd.services.gitlab-runner = {
    #   description = "Gitlab Runner";
    #   documentation = [ "https://docs.gitlab.com/runner/" ];
    #   after = [ "network.target" ]
    #     ++ optional hasDocker "docker.service";
    #   requires = optional hasDocker "docker.service";
    #   wantedBy = [ "multi-user.target" ];
    #   environment = config.networking.proxy.envVars // {
    #     HOME = "/var/lib/gitlab-runner";
    #   };
    #   path = with pkgs; [
    #     bash
    #     gawk
    #     jq
    #     moreutils
    #     remarshal
    #     util-linux
    #     cfg.package
    #   ] ++ cfg.extraPackages;
    #   reloadIfChanged = true;
    #   serviceConfig = {
    #     # Set `DynamicUser` under `systemd.services.gitlab-runner.serviceConfig`
    #     # to `lib.mkForce false` in your configuration to run this service as root.
    #     # You can also set `User` and `Group` options to run this service as desired user.
    #     # Make sure to restart service or changes won't apply.
    #     DynamicUser = true;
    #     StateDirectory = "gitlab-runner";
    #     SupplementaryGroups = optional hasDocker "docker";
    #     ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
    #     ExecStart = "${startScript}/bin/gitlab-runner-start";
    #     ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
    #   } // optionalAttrs (cfg.gracefulTermination) {
    #     TimeoutStopSec = "${cfg.gracefulTimeout}";
    #     KillSignal = "SIGQUIT";
    #     KillMode = "process";
    #   };
    # };
    # # Enable docker if `docker` executor is used in any service
    # virtualisation.docker.enable = mkIf (
    #   any (s: s.executor == "docker") (attrValues cfg.services)
    # ) (mkDefault true);
  };
  imports = [
    (mkRenamedOptionModule [ "services" "gitlab-runner" "packages" ] [ "services" "gitlab-runner" "extraPackages" ] )
    (mkRemovedOptionModule [ "services" "gitlab-runner" "configOptions" ] "Use services.gitlab-runner.services option instead" )
    (mkRemovedOptionModule [ "services" "gitlab-runner" "workDir" ] "You should move contents of workDir (if any) to /var/lib/gitlab-runner" )
  ];
}