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

with lib;

let
  cfg = config.system.rebuild-machine;

  sshConfig = pkgs.writeText "config" ''
    UserKnownHostsFile ${knownHostsFile}

    Host ${cfg.repoHost}
      User ${cfg.repoUser}
      IdentityFile ${if isNull cfg.sopsConfig then cfg.repoPrivkey else config.sops.secrets."${cfg.sopsName}".path}
      IdentitiesOnly yes
  '';

  knownHostsFile = pkgs.writeText "known_hosts" (concatMapStringsSep "\n" (kPath: cfg.repoHost + " " + readFile kPath) (attrValues cfg.repoPubkeys));
  
  rebuildScript = pkgs.stdenv.mkDerivation {
    name = "rebuild-${hostName}";

    src = ./rebuild-machine.zsh;

    buildInputs = with pkgs; [ makeWrapper ];

    phases = [ "buildPhase" "installPhase" ];

    inherit (pkgs) zsh coreutils openssh;
    inherit (cfg) scriptName;
    inherit (cfg.flake) flakeOutput;
    flake = cfg.flake.name;
    nixosRebuild = config.system.build.nixos-rebuild;
    inherit (config.security) wrapperDir;
    inherit sshConfig;

    buildPhase = ''
      substituteAll $src rebuild-machine.zsh
    '';

    installPhase = ''
      mkdir -p $out/bin
      install -m 0755 rebuild-machine.zsh $out/bin/${cfg.scriptName}
    '';
  };
in {
  options = {
    system.rebuild-machine = {
      scriptName = mkOption {
        type = types.str;
        default = "rebuild-${hostName}";
        description = ''
          Name of the script wrapping <literal>nixos-rebuild</literal>
        '';
      };
      
      flake = mkOption {
        type = types.submodule {
          options = {
            flake = mkOption {
              type = types.attrs;
            };
            flakeOutput = mkOption {
              type = types.str;
            };
            name = mkOption {
              type = types.str;
              default = "machines";
            };
          };
        };
        default = { flake = { type = "git"; url = "ssh://${cfg.repoHost}/nixos"; ref = "flakes"; }; flakeOutput = hostName; };
        description = ''
          The Flake URI of the NixOS configuration to build.
        '';
      };

      repoHost = mkOption {
        type = types.str;
        default = "git.yggdrasil.li";
      };
      
      repoUser = mkOption {
        type = types.str;
        default = "gitolite";
      };

      repoPubkeys = mkOption {
        type = types.attrsOf types.path;
        default = genAttrs ["rsa" "ed25519"] (kType: ./ssh-pub + "/${cfg.repoHost}-${kType}.pub");
      };

      repoPrivkey = mkOption {
        type = types.path;
        default = ./ssh + "/${hostName}/private";
      };

      sopsName = mkOption {
        type = types.nullOr types.str;
        default = "rebuild-machines";
      };

      sopsConfig = mkOption {
        type = types.nullOr types.attrs;
        default = {
          format = "binary";
        };
      };

      period = mkOption {
        type = types.nullOr types.str;
        description = "Call <code>rebuild-${hostName} boot</code> periodically";
        default = null;
        example = "hourly";
      };
    };
  };
  
  config = {
    assertions = [
      { assertion = isNull cfg.sopsConfig || (!(isNull cfg.sopsName));
        message = "If option sopsConfig is not null option sopsName may not be null";
      }
    ];
    
    sops.secrets = mkIf (!(isNull cfg.sopsConfig)) {
      "${cfg.sopsName}" = {
        sopsFile = cfg.repoPrivkey;
      } // cfg.sopsConfig;
    };

    environment.systemPackages = [rebuildScript];

    systemd.services."rebuild-${hostName}" = mkIf (cfg.period != null) {
      description = "Upgrade System on Next Boot";
      requisite = [ "network.target" ];
      after = [ "network.target" ];

      restartIfChanged = false;
      unitConfig.X-StopOnRemoval = false;

      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${rebuildScript}/bin/${cfg.scriptName} boot";
      };
    };

    systemd.timers."rebuild-${hostName}" = mkIf (cfg.period != null) {
      wantedBy = [ "timers.target" ];
      timerConfig = {
        OnCalendar = cfg.period;
        Persistent = true;
      };
    };

    nix.registry = mkIf (cfg.flake != null) {
      ${cfg.flake.name} = {
        exact = false;
        to = cfg.flake.flake;
      };
    };
  };
}