{ config, pkgs, ... }:

with pkgs.lib;

let
  cfg = config.services.hydra;

  hydraConf = pkgs.writeScript "hydra.conf"
    ''
      using_frontend_proxy 1
      base_uri ${cfg.hydraURL}
      notification_sender ${cfg.notificationSender}
      max_servers 25
    '';

  env = ''export NIX_REMOTE=daemon ''
      + ''HYDRA_DBI="${cfg.dbi}" ''
      + ''HYDRA_CONFIG=${cfg.baseDir}/data/hydra.conf ''
      + ''HYDRA_DATA=${cfg.baseDir}/data ''
      + ''HYDRA_PORT="${toString cfg.port}" ''
      + (if cfg.logo != null
         then ''HYDRA_LOGO="${cfg.logo}" ''
         else "")
      + ''HYDRA_TRACKER="${cfg.tracker}" ;'';

in

{
  ###### interface
  options = {
    services.hydra = rec {

      enable = mkOption {
        default = false;
        description = ''
          Whether to run Hydra services.
        '';
      };

      baseDir = mkOption {
        default = "/home/${user.default}";
        description = ''
          The directory holding configuration, logs and temporary files.
        '';
      };

      user = mkOption {
        default = "hydra";
        description = ''
          The user the Hydra services should run as.
        '';
      };

      dbi = mkOption {
        default = "dbi:Pg:dbname=hydra;host=localhost;user=root;";
        example = "dbi:SQLite:/home/hydra/db/hydra.sqlite";
        description = ''
          The DBI string for Hydra database connection.
        '';
      };

      hydra = mkOption {
        default = pkgs.hydra;
        description = ''
          Location of hydra
        '';
      };

      hydraURL = mkOption {
        default = "http://hydra.nixos.org";
        description = ''
          The base URL for the Hydra webserver instance. Used for links in emails.
        '';
      };

      port = mkOption {
        default = 3000;
        description = ''
          TCP port the web server should listen to.
        '';
      };

      minimumDiskFree = mkOption {
        default = 5;
        description = ''
          Threshold of minimum disk space (G) to determine if queue runner should run or not.
        '';
      };

      minimumDiskFreeEvaluator = mkOption {
        default = 2;
        description = ''
          Threshold of minimum disk space (G) to determine if evaluator should run or not.
        '';
      };

      notificationSender = mkOption {
        default = "e.dolstra@tudelft.nl";
        description = ''
          Sender email address used for email notifications.
        '';
      };

      tracker = mkOption {
        default = "";
        description = ''
          Piece of HTML that is included on all pages.
        '';
      };

      logo = mkOption {
        default = null;
        description = ''
          File name of an alternate logo to be displayed on the web pages.
        '';
      };

      autoStart = mkOption {
        default = true;
        description = ''
          If hydra upstart jobs should start automatically.
        '';
      };

    };

  };


  ###### implementation

  config = mkIf cfg.enable {
    environment.systemPackages = [ cfg.hydra ];

    users.extraUsers = [
      { name = cfg.user;
        description = "Hydra";
        home = cfg.baseDir;
        createHome = true;
        useDefaultShell = true;
      }
    ];

    # We have our own crontab entries for GC, see below.
    nix.gc.automatic = false;

    nix.extraOptions = ''
      gc-keep-outputs = true
      gc-keep-derivations = true

      # The default (`true') slows Nix down a lot since the build farm
      # has so many GC roots.
      gc-check-reachability = false

      # Hydra needs caching of build failures.
      build-cache-failure = true

      build-poll-interval = 10

      use-sqlite-wal = false
    '';

    jobs.hydra_init =
      { description = "hydra-init";
        startOn = "started network-interfaces";
        preStart = ''
          mkdir -p ${cfg.baseDir}/data
          chown ${cfg.user} ${cfg.baseDir}/data
          ln -sf ${hydraConf} ${cfg.baseDir}/data/hydra.conf
        '';
        exec = ''
          echo done
        '';
      };

    jobs.hydra_server =
      { description = "hydra-server";
        startOn = if cfg.autoStart then "started network-interfaces hydra-init" else "never";
        exec = ''
          ${pkgs.su}/bin/su - ${cfg.user} -c '${env} ${cfg.hydra}/bin/hydra-server > ${cfg.baseDir}/data/server.log 2>&1'
        '';
      };

    jobs.hydra_queue_runner =
      { description = "hydra-queue-runner";
        startOn = if cfg.autoStart then "started network-interfaces hydra-init" else "never";
        preStart = "${pkgs.su}/bin/su - ${cfg.user} -c '${env} ${cfg.hydra}/bin/hydra-queue-runner --unlock'";
        exec = ''
          ${pkgs.su}/bin/su - ${cfg.user} -c '${env} nice -n 8 ${cfg.hydra}/bin/hydra-queue-runner > ${cfg.baseDir}/data/queue-runner.log 2>&1'
        '';
      };

    jobs.hydra_evaluator =
      { description = "hydra-evaluator";
        startOn = if cfg.autoStart then "started network-interfaces hydra-init" else "never";
        exec = ''
          ${pkgs.su}/bin/su - ${cfg.user} -c '${env} nice -n 5 ${cfg.hydra}/bin/hydra-evaluator > ${cfg.baseDir}/data/evaluator.log 2>&1'
        '';
      };

    services.cron.systemCronJobs =
      let
        # If there is less than ... GiB of free disk space, stop the queue
        # to prevent builds from failing or aborting.
        checkSpace = pkgs.writeScript "hydra-check-space"
          ''
            #! /bin/sh
            if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFree} * 1024**3)) ]; then
          stop hydra_queue_runner
            fi
        if [ $(($(stat -f -c '%a' /nix/store) * $(stat -f -c '%S' /nix/store))) -lt $((${toString cfg.minimumDiskFreeEvaluator} * 1024**3)) ]; then
          stop hydra_evaluator
        fi
          '';

        collect = pkgs.writeScript "collect-some-garbage"
          # Arrange to always have at least 100 GiB free.
          '' #!/bin/sh -e
             available="$(df -B1 /nix/store | tail -n 1 | awk '{ print $4 }')"
             target="$((100 * 1024**3))"
             to_free="$(($available > $target ? 200 * 1024**2 : $target - $available))"

             echo "$available B available, and targeting $target B available"
             echo "thus, freeing $to_free B"
             exec "${pkgs.nix}/bin/nix-collect-garbage" --max-freed "$to_free"
          '';

        compressLogs = pkgs.writeScript "compress-logs" ''
            #! /bin/sh -e
           touch -d 'last month' r
           find /nix/var/log/nix/drvs -type f -a ! -newer r -name '*.drv' | xargs bzip2 -v
         '';
      in
        [ "15 03 * * * root  ${collect} &> ${cfg.baseDir}/data/gc.log"
          "15 13 * * * root  ${collect} &> ${cfg.baseDir}/data/gc.log"

          "*/5 * * * * root  ${checkSpace} &> ${cfg.baseDir}/data/checkspace.log"

          "15 5 * * * root  ${compressLogs} &> ${cfg.baseDir}/data/compress.log"
          "15 02 * * * ${cfg.user} ${env} ${cfg.hydra}/bin/hydra-update-gc-roots &> ${cfg.baseDir}/data/gc-roots.log"
        ];
  };
}