# This is the declarative host management converted into a flake-parts module.
# It also enforces a structure for declarative NixOS setups such a deploy-rs
# node, a hostname and an optional domain, and deploy-rs-related options so it
# isn't really made to be generic or anything like that.
{ config, options, lib, inputs, ... }:

let
  cfg = config.setups.nixos;
  partsConfig = config;

  # A thin wrapper around the NixOS configuration function.
  mkHost =
    { pkgs, lib ? pkgs.lib, system, extraModules ? [ ], specialArgs ? { }, }:
    let
      # Evaluating the system ourselves (which is trivial) instead of relying
      # on nixpkgs.lib.nixosSystem flake output.
      nixosSystem = args: import "${pkgs.path}/nixos/lib/eval-config.nix" args;
    in (lib.makeOverridable nixosSystem) {
      inherit pkgs specialArgs;
      modules = extraModules
        ++ [{ nixpkgs.hostPlatform = lib.mkForce system; }];

      # Since we're setting it through nixpkgs.hostPlatform, we'll have to pass
      # this as null.
      system = null;
    };

  # The nixos-generators modules set as well as our custom-made ones.
  nixosGeneratorsModulesSet = let
    importNixosGeneratorModule = (_: modulePath: {
      imports = [ modulePath "${inputs.nixos-generators}/format-module.nix" ];
    });

    customFormats = lib.mapAttrs importNixosGeneratorModule {
      install-iso-graphical = ../../nixos-generators/install-iso-graphical.nix;
    };
  in inputs.nixos-generators.nixosModules // customFormats;

  # A very very thin wrapper around `mkHost` to build with the given format.
  mkImage = { pkgs, system, extraModules ? [ ], format ? "iso", specialArgs ? { } }:
    let
      extraModules' = extraModules ++ [ nixosGeneratorsModulesSet.${format} ];
      image = mkHost {
        inherit pkgs system specialArgs;
        extraModules = extraModules';
      };
    in image.config.system.build.${image.config.formatAttr};

  deployNodeType = { config, lib, ... }: {
    freeformType = with lib.types; attrsOf anything;
    imports = [ ./shared/deploy-node-type.nix ];

    options = {
      profiles = lib.mkOption {
        type = with lib.types; functionTo (attrsOf anything);
        default = os: {
          system = {
            sshUser = "admin";
            user = "root";
            path = inputs.deploy.lib.${os.system}.activate.nixos os.config;
          };
        };
        defaultText = lib.literalExpression ''
          os: {
            system = {
              sshUser = "root";
              user = "admin";
              path = <deploy-rs>.lib.''${os.system}.activate.nixos os.config;
            };
          }
        '';
        description = ''
          A set of profiles for the resulting deploy node.

          Since each config can result in more than one NixOS system, it has to
          be a function where the passed argument is an attribute set with the
          following values:

          * `name` is the attribute name from `configs`.
          * `config` is the NixOS configuration itself.
          * `system` is a string indicating the platform of the NixOS system.

          If unset, it will create a deploy-rs node profile called `system`
          similar to those from nixops.
        '';
      };
    };
  };

  configType = { options, config, name, lib, ... }:
    let setupConfig = config;
    in {
      options = {
        formats = lib.mkOption {
          type = with lib.types; nullOr (listOf str);
          default = [ "iso" ];
          description = ''
            The image formats to be generated from nixos-generators. When given
            as `null`, it is listed as part of `nixosConfigurations` and excluded
            from `images` flake output which is often the case for desktop NixOS
            systems.
          '';
        };

        hostname = lib.mkOption {
          type = lib.types.nonEmptyStr;
          default = name;
          example = "MyWhatNow";
          description = "The hostname of the NixOS configuration.";
        };

        domain = lib.mkOption {
          type = with lib.types; nullOr nonEmptyStr;
          default = null;
          example = "work.example.com";
          description = "The domain of the NixOS system.";
        };

        deploy = lib.mkOption {
          type = with lib.types; nullOr (submodule deployNodeType);
          default = null;
          description = ''
            deploy-rs node settings for the resulting NixOS configuration. When
            this attribute is given with a non-null value, it will be included in
            `nixosConfigurations` even if
            {option}`setups.nixos.configs.<config>.formats` is set.
          '';
          example = {
            hostname = "work1.example.com";
            fastConnection = true;
            autoRollback = true;
            magicRollback = true;
            remoteBuild = true;
          };
        };

        shouldBePartOfNixOSConfigurations = lib.mkOption {
          type = lib.types.bool;
          default = lib.isAttrs config.deploy || config.formats == null;
          example = true;
          description = ''
            Indicates whether the declarative NixOS setup should be included as
            part of the `nixosConfigurations` flake output.
          '';
        };
      };

      config.nixpkgs.config = cfg.sharedNixpkgsConfig;
      config.specialArgs = cfg.sharedSpecialArgs;

      config.modules = [
        # Bring in the required modules.
        "${partsConfig.setups.configDir}/nixos/${config.configName}"

        # Setting up the typical configuration.
        ({ config, lib, ... }: {
          config = lib.mkMerge [
            {
              nixpkgs.overlays = setupConfig.nixpkgs.overlays;
              networking.hostName = lib.mkDefault setupConfig.hostname;
            }

            (lib.mkIf (setupConfig.domain != null) {
              networking.domain = lib.mkDefault setupConfig.domain;
            })
          ];
        })
      ];
    };
in {
  options.setups.nixos = {
    sharedNixpkgsConfig = options.setups.sharedNixpkgsConfig // {
      description = ''
        Shared configuration between all of the nixpkgs instance of the
        declarative NixOS systems.

        ::: {.note}
        This is implemented since the way how NixOS systems built here are made
        with initializing a nixpkgs instance ourselves and NixOS doesn't allow
        configuring the nixpkgs instances that are already defined outside of
        its module environment.
        :::
      '';
    };

    sharedSpecialArgs = options.setups.sharedSpecialArgs // {
      description = ''
        Shared set of module arguments as part of `_module.specialArgs` of the
        configuration.
      '';
    };

    sharedModules = lib.mkOption {
      type = with lib.types; listOf deferredModule;
      default = [ ];
      description = ''
        A list of modules to be shared by all of the declarative NixOS setups.
      '';
    };

    configs = lib.mkOption {
      type = with lib.types;
        attrsOf (submodule [
          (import ./shared/nix-conf.nix { inherit inputs; })
          (import ./shared/config-options.nix { inherit (config) systems; })
          ./shared/nixpkgs-options.nix
          ./shared/special-args-options.nix
          configType
        ]);
      default = { };
      description = ''
        An attribute set of metadata for the declarative NixOS setups. This
        will then be used for related flake outputs such as
        `nixosConfigurations` and `images`.

        ::: {.note}
        For `nixosConfigurations` output, each of them is a pure NixOS
        configuration where `nixpkgs.hostPlatform` is set and each of the
        config is renamed into `$CONFIGNAME-$SYSTEM` if the host is configured
        to have more than one system.
        :::
      '';
      example = lib.literalExpression ''
        {
          desktop = {
            systems = [ "x86_64-linux" "aarch64-linux" ];
            formats = null;
            modules = [
              inputs.nur.nixosModules.nur
            ];
            nixpkgs = {
              branch = "nixos-unstable";
              overlays = [
                # Neovim nightly!
                inputs.neovim-nightly-overlay.overlays.default

                # Emacs unstable version!
                inputs.emacs-overlay.overlays.default

                # Helix master!
                inputs.helix-editor.overlays.default

                # Access to NUR.
                inputs.nur.overlay
              ];
            };
          };

          server = {
            systems = [ "x86_64-linux" "aarch64-linux" ];
            domain = "work.example.com";
            formats = [ "do" "linode" ];
            nixpkgs.branch = "nixos-unstable-small";
            deploy = {
              autoRollback = true;
              magicRollback = true;
            };
          };

          vm = {
            systems = [ "x86_64-linux" "aarch64-linux" ];
            formats = [ "vm" ];
          };
        }
      '';
    };
  };

  config = lib.mkMerge [
    {
      setups.nixos.sharedNixpkgsConfig = config.setups.sharedNixpkgsConfig;

      setups.nixos.sharedSpecialArgs = config.setups.sharedSpecialArgs;
    }

    (lib.mkIf (cfg.configs != { }) {

      flake = let
        # A quick data structure we can pass through multiple build pipelines.
        pureNixosConfigs = let
          validConfigs =
            lib.filterAttrs (_: v: v.shouldBePartOfNixOSConfigurations)
            cfg.configs;

          generatePureConfigs = hostname: metadata:
            lib.listToAttrs (lib.map (system:
              let
                nixpkgs = inputs.${metadata.nixpkgs.branch};

                # We won't apply the overlays here since it is set
                # modularly.
                pkgs = import nixpkgs {
                  inherit system;
                  inherit (metadata.nixpkgs) config;
                };
              in lib.nameValuePair system (mkHost {
                inherit pkgs system;
                inherit (metadata) specialArgs;
                extraModules = cfg.sharedModules ++ metadata.modules;
              })) metadata.systems);
        in lib.mapAttrs generatePureConfigs validConfigs;
      in {
        nixosConfigurations = let
          renameSystem = name: system: config:
            lib.nameValuePair "${name}-${system}" config;
        in lib.concatMapAttrs
        (name: configs: lib.mapAttrs' (renameSystem name) configs)
        pureNixosConfigs;

        deploy.nodes = let
          validConfigs =
            lib.filterAttrs (name: _: cfg.configs.${name}.deploy != null)
            pureNixosConfigs;

          generateDeployNode = name: system: config:
            lib.nameValuePair "nixos-${name}-${system}"
            (let deployConfig = cfg.configs.${name}.deploy;
            in deployConfig // {
              profiles = cfg.configs.${name}.deploy.profiles {
                inherit name config system;
              };
            });
        in lib.concatMapAttrs
        (name: configs: lib.mapAttrs' (generateDeployNode name) configs)
        validConfigs;
      };

      perSystem = { system, lib, ... }: {
        images = let
          validImages = lib.filterAttrs (host: metadata:
            metadata.formats != null && (lib.elem system metadata.systems))
            cfg.configs;

          generateImages = name: metadata:
            let
              buildImage = format:
                lib.nameValuePair "${name}-${format}" (mkImage {
                  inherit format system;
                  inherit (metadata) specialArgs;
                  pkgs = import inputs.${metadata.nixpkgs.branch} {
                    inherit system;
                    inherit (metadata.nixpkgs) config;
                  };
                  extraModules = cfg.sharedModules ++ metadata.modules;
                });

              images = lib.map buildImage metadata.formats;
            in lib.listToAttrs images;
        in lib.concatMapAttrs generateImages validImages;
      };
    })
];
}
