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

with lib;

let
  ascensionsDir = "/var/lib/ascensions";

  ascensionType = { name, ... }: {
    options = {
      incantations = mkOption {
        type = with types; functionTo (listOf package);
        default = [];
      };
      distributed = mkOption {
        type = types.bool;
        default = false;
        description = "Whether to perform a distributed ascension using Consul KV.";
      };
      requiredBy = mkOption {
        type = with types; listOf str;
        default = [ "${name}.service" ];
        description = "Services that require this ascension.";
      };
      before = mkOption {
        type = with types; listOf str;
        default = [];
        description = "Run the ascension before these services.";
      };
      after = mkOption {
        type = with types; listOf str;
        default = [];
        description = "Run the ascension after these services.";
      };
    };
  };

  builtinIncantations = with pkgs; rec {
    execShellWith = extraPackages: script: writeShellScript "incantation" ''
      export PATH='${makeBinPath ([ coreutils ] ++ extraPackages)}'
      ${script}
    '';

    execShell = execShellWith [];

    multiple = incantations: execShell (concatStringsSep " && " incantations);

    move = from: to: execShell ''
      test -e ${escapeShellArg from} && rm -rf ${escapeShellArg to}
      mkdir -p "$(dirname ${escapeShellArg to})"
      mv ${escapeShellArgs [ from to ]}
    '';

    chmod = mode: target: "chmod -R ${escapeShellArgs [ mode target ]}";
  };

  allIncantations = builtinIncantations // mapAttrs (_: mk: mk allIncantations) config.system.extraIncantations;

  runIncantations = f: f allIncantations;

  consul = config.services.consul.package;
in

{
  options.system = {
    ascensions = mkOption {
      type = with types; attrsOf (submodule ascensionType);
      default = {};
    };
    extraIncantations = mkOption {
      type = with types; attrsOf (functionTo raw);
      default = {};
    };
  };

  config = {
    systemd = {
      tmpfiles.rules = [
        "d ${ascensionsDir} 0755 root root - -"
      ];

      services = mapAttrs' (name: asc: {
        name = "ascend-${name}";
        value = let
          incantations = runIncantations asc.incantations;
          targetLevel = toString (length incantations);
        in {
          description = "Ascension for ${name}";
          wantedBy = [ "multi-user.target" ];
          inherit (asc) requiredBy before;
          after = asc.after ++ (lib.optional asc.distributed "consul-ready.service");
          requires = lib.optional asc.distributed "consul-ready.service";
          serviceConfig.Type = "oneshot";
          distributed.enable = asc.distributed;
          script = ''
            incantations=(${concatStringsSep " " incantations})
            ${if asc.distributed then /*bash*/ ''
              getLevel() {
                ${consul}/bin/consul kv get 'ascensions/${name}/currentLevel'
              }
              setLevel() {
                ${consul}/bin/consul kv put 'ascensions/${name}/currentLevel' "$1" >/dev/null
              }
              isEmpty() {
                ! getLevel >/dev/null 2>/dev/null
              }
            '' else /*bash*/ ''
              levelFile='${ascensionsDir}/${name}'
              getLevel() {
                echo "$(<"$levelFile")"
              }
              setLevel() {
                echo "$1" > "$levelFile"
              }
              isEmpty() {
                [[ ! -e "$levelFile" ]]
              }
            ''
            }
            if isEmpty; then
              setLevel '${targetLevel}'
              echo Initializing at level ${targetLevel}
              exit 0
            fi
            cur=$(getLevel)
            echo Current level: $cur
            for lvl in $(seq $(($cur+1)) ${targetLevel}); do
              echo Running incantation for level $lvl...
              ''${incantations[$((lvl-1))]}
              setLevel "$lvl"
            done
            echo All incantations complete, ascended to level $(getLevel)
          '';
        };
      }) config.system.ascensions;
    };
  };
}