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

with lib;
let
  cfg = config.fudo.secrets;

  encrypt-on-disk = { secret-name, target-host, target-pubkey, source-file }:
    pkgs.stdenv.mkDerivation {
      name = "${target-host}-${secret-name}-secret";
      phases = "installPhase";
      buildInputs = [ pkgs.age ];
      installPhase = ''
        age -a -r "${target-pubkey}" -o $out ${source-file}
      '';
    };

  decrypt-script = { secret-name, source-file, target-host, target-file
    , host-master-key, user, group, permissions }:
    pkgs.writeShellScript
    "decrypt-fudo-secret-${target-host}-${secret-name}.sh" ''
      rm -rf ${target-file}
      age -d -i ${host-master-key.key-path} -o ${target-file} ${
        encrypt-on-disk {
          inherit secret-name source-file target-host;
          target-pubkey = host-master-key.public-key;
        }
      }
      chown ${user}:${group} ${target-file}
      chmod ${permissions} ${target-file}
    '';

  secret-service = target-host: secret-name:
    { source-file, target-file, user, group, permissions }: {
      description = "decrypt secret ${secret-name} for ${target-host}.";
      wantedBy = [ "default.target" ];
      serviceConfig = {
        Type = "oneshot";
        ExecStartPre = pkgs.writeShellScript
          "prepare-${target-host}-${secret-name}-secret-dir.sh" ''
            TARGET_DIR=$(dirname ${target-file})
            if [[ ! -d "$TARGET_DIR" ]]; then
              mkdir -p "$TARGET_DIR"
            fi
          '';
        ExecStart = let
          host-master-key = config.fudo.hosts.${target-host}.master-key;
        in decrypt-script {
          inherit secret-name source-file target-host target-file host-master-key
            user group permissions;
        };
      };
      path = [ pkgs.age ];
    };

  secretOpts = { ... }: {
    options = with types; {
      source-file = mkOption {
        type = path; # CAREFUL: this will copy the file to nixstore...keep on deploy host
        description = "File from which to load the secret.";
      };

      target-file = mkOption {
        type = str;
        description =
          "Target file on the host; the secret will be decrypted to this file.";
      };

      user = mkOption {
        type = str;
        description = "User (on target host) to which the file will belong.";
      };

      group = mkOption {
        type = str;
        description = "Group (on target host) to which the file will belong.";
        default = "nogroup";
      };

      permissions = mkOption {
        type = str;
        description = "Permissions to set on the target file.";
        default = "0400";
      };
    };
  };

  nix-build-users = let usernames = attrNames config.users.users;
  in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null)
  usernames;

in {
  options.fudo.secrets = with types; {
    enable = mkOption {
      type = bool;
      description = "Include secrets in the build (disable when secrets are unavailable)";
      default = true;
    };

    host-secrets = mkOption {
      type = attrsOf (attrsOf (submodule secretOpts));
      description = "Map of hosts to host secrets";
      default = { };
    };

    host-deep-secrets = mkOption {
      type = attrsOf (attrsOf (submodule secretOpts));
      description = ''
        Secrets that are only passed during deployment.

        These secrets will be passed as nixops deployment secrets,
        _unlike_ regular secrets that are passed to hosts as part of
        the nixops store, but encrypted with the host SSH key. Regular
        secrets are kept secret from normal users. These secrets will
        be kept secret from _everybody_. However, they won't be
        available on the host at boot until a new deployment occurs.
      '';
      default = { };
    };

    secret-users = mkOption {
      type = listOf str;
      description = "List of users with read-access to secrets.";
      default = [ ];
    };

    secret-group = mkOption {
      type = str;
      description = "Group to which secrets will belong.";
      default = "nixops-secrets";
    };

    secret-paths = mkOption {
      type = listOf str;
      description =
        "Paths which contain (only) secrets. The contents will be reabable by the secret-group.";
      default = [ ];
    };
  };

  config = mkIf cfg.enable {
    users.groups = {
      ${cfg.secret-group} = { members = cfg.secret-users ++ nix-build-users; };
    };

    systemd = let
      hostname = config.instance.hostname;
      host-secrets = if (hasAttr hostname cfg.host-secrets) then
        cfg.host-secrets.${hostname}
      else
        { };
      host-secret-services = mapAttrs' (secret: secretOpts:
        (nameValuePair "fudo-secret-${hostname}-${secret}"
          (secret-service hostname secret secretOpts))) host-secrets;

    in {
      services = host-secret-services // {
        fudo-secrets-watcher = {
          wantedBy = [ "default.target" ];
          description =
            "Ensure access for group ${cfg.secret-group} to fudo secret paths.";
          serviceConfig = {
            ExecStart = pkgs.writeShellScript "fudo-secrets-watcher.sh"
              (concatStringsSep "\n" (map (path: ''
                chown -R root:${cfg.secret-group} ${path}
                chmod -R u=rwX,g=rX,o= ${path}
              '') cfg.secret-paths));
          };
        };
      };

      paths.fudo-secrets-watcher = mkIf ((length cfg.secret-paths) > 0) {
        wantedBy = [ "default.target" ];
        description = "Watch fudo secret paths, and correct perms on changes.";
        pathConfig = {
          PathChanged = cfg.secret-paths;
          Unit = "fudo-secrets-watcher.service";
        };
      };

      tmpfiles.rules = map (path: "d '${path}' - root ${cfg.secret-group} - -")
        cfg.secret-paths;
    };
  };
}