# This is the declarative user management converted into a flake-parts module.
# Take note, it reinforces mandatory import of home-manager to its composed
# environments such as NixOS.
{ config, options, lib, inputs, ... }:

let
  cfg = config.setups.home-manager;
  partsConfig = config;

  # A thin wrapper around the home-manager configuration function.
  mkHome = { pkgs, lib ? pkgs.lib, system, homeManagerBranch ? "home-manager"
    , extraModules ? [ ], specialArgs ? { } }:
    inputs.${homeManagerBranch}.lib.homeManagerConfiguration {
      inherit pkgs lib;
      extraSpecialArgs = specialArgs;
      modules = extraModules;
    };

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

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

          Since each config can result in more than one home-manager
          environment, 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 home-manager configuration itself.
          * `system` is a string indicating the platform of the NixOS system.

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

  configType = { config, name, lib, ... }: {
    options = {
      homeManagerBranch = lib.mkOption {
        type = lib.types.str;
        default = "home-manager";
        example = "home-manager-stable";
        description = ''
          The home-manager branch to be used for the NixOS module. By default,
          it will use the `home-manager` flake input.
        '';
      };

      homeDirectory = lib.mkOption {
        type = lib.types.path;
        default = "/home/${name}";
        example = "/var/home/public-user";
        description = ''
          The home directory of the home-manager user.
        '';
      };

      deploy = lib.mkOption {
        type = with lib.types;
          nullOr (submoduleWith {
            specialArgs = { username = name; };
            modules = [ deploySettingsType ];
          });
        default = null;
        description = ''
          deploy-rs settings to be passed onto the home-manager configuration
          node.
        '';
      };

      standaloneConfigModules = options.setups.home-manager.standaloneConfigModules // {
        description = ''
          A list of config-specific modules to be used when deployed as a
          standalone home-manager environment.
        '';
      };
    };

    config = {
      modules = [
        "${partsConfig.setups.configDir}/home-manager/${config.configName}"

        (let setupConfig = config;
        in { config, lib, ... }: {
          nixpkgs.overlays = setupConfig.nixpkgs.overlays;
          home.username = lib.mkForce name;
          home.homeDirectory = lib.mkForce setupConfig.homeDirectory;
        })
      ];

      nixpkgs.config = cfg.sharedNixpkgsConfig;
      specialArgs = cfg.sharedSpecialArgs;
    };
  };
in {
  options.setups.home-manager = {
    sharedNixpkgsConfig = options.setups.sharedNixpkgsConfig // {
      description = ''
        nixpkgs configuration to be shared among home-manager configurations
        defined here.
      '';
    };

    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 home-manager
        setups.

        ::: {.note}
        Note this will be shared into NixOS as well through the home-manager
        NixOS module.
        :::
      '';
    };

    standaloneConfigModules = lib.mkOption {
      type = with lib.types; listOf deferredModule;
      default = [ ];
      internal = true;
      description = ''
        A list of modules to be added alongside the shared home-manager modules
        in the standalone home-manager configurations.

        This is useful for modules that are only suitable for standalone
        home-manager configurations compared to home-manager configurations
        used as a NixOS module.
      '';
    };

    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 home-manager setups.
      '';
      example = lib.literalExpression ''
        {
          foo-dogsquared = {
            systems = [ "aarch64-linux" "x86_64-linux" ];
            modules = [
              inputs.nur.hmModules.nur
              inputs.nixvim.homeManagerModules.nixvim
            ];
            nixpkgs.overlays = [
              inputs.neovim-nightly-overlay.overlays.default
              inputs.emacs-overlay.overlays.default
              inputs.helix-editor.overlays.default
              inputs.nur.overlay
            ];
          };

          plover.systems = [ "x86_64-linux" ];
        }
      '';
    };
  };

  # Setting up all of the integrations for the wider-scoped environments.
  options.setups.nixos.configs = lib.mkOption {
    type = with lib.types;
      attrsOf (submodule [
        ./shared/home-manager-users.nix

        ({ config, lib, name, ... }:
          let
            inherit (config.home-manager) nixpkgsInstance;
            setupConfig = config;

            hasHMBranch = config.home-manager.branch != null;
            hasHomeManagerUsers = config.home-manager.users != { };
            isNixpkgs = state: hasHomeManagerUsers && nixpkgsInstance == state;
            homeManagerUserType = { name, config, lib, ... }: {
              options = {
                userConfig = lib.mkOption {
                  type = with lib.types; attrsOf anything;
                  description = ''
                    The configuration applied for individual users set in the
                    wider-scoped environment.
                  '';
                };
              };

              config = let
                hmUserConfig = partsConfig.setups.home-manager.configs.${name};
              in {
                # The rationale for this is we're making sure that it is
                # synced with the NixOS user settings.
                userConfig = {
                  isNormalUser = lib.mkDefault true;
                  createHome = lib.mkDefault true;
                  home = lib.mkForce hmUserConfig.homeDirectory;
                };

                additionalModules = [
                  ({ lib, ... }: {
                    home.homeDirectory = lib.mkForce hmUserConfig.homeDirectory;
                    home.username = lib.mkForce name;
                  })
                ];
              };
            };
          in {
            options.home-manager = {
              users = lib.mkOption {
                type = with lib.types; attrsOf (submodule homeManagerUserType);
              };

              nixpkgsInstance = lib.mkOption {
                type = lib.types.enum [ "global" "separate" "none" ];
                default = "global";
                description = ''
                  Indicates how to manage the nixpkgs instance (or instances)
                  of the holistic system. This will also dictate how to import
                  overlays from
                  {option}`setups.home-manager.configs.<user>.overlays`.

                  * `global` enforces to use one nixpkgs instance for all
                  home-manager users and imports all of the overlays into the
                  nixpkgs instance of the NixOS system.

                  * `separate` enforces the NixOS system to use individual
                  nixpkgs instance for all home-manager users and imports the
                  overlays to the nixpkgs instance of the home-manager user.

                  * `none` leave the configuration alone and do not import
                  overlays at all where you have to set them yourself. This is
                  the best option if you want more control over each individual
                  NixOS and home-manager configuration.

                  The default value is set to `global` which is the encouraged
                  practice with this module.
                '';
              };
            };

            # Mapping the declarative home-manager users (if it has one) into NixOS
            # users.
            config = {
              modules = [
                (lib.mkIf hasHMBranch ({ lib, ... }: {
                  imports = [
                    # For declarative NixOS systems, importing home-manager module is
                    # mandatory.
                    inputs.${config.home-manager.branch}.nixosModules.home-manager
                  ];

                  # Set the home-manager-related settings.
                  home-manager.sharedModules =
                    partsConfig.setups.home-manager.sharedModules;

                  # These are just the recommended options for home-manager that may be
                  # the default value in the future but this is how most of the NixOS
                  # setups are already done so...
                  home-manager.useUserPackages = lib.mkDefault true;
                  home-manager.useGlobalPkgs = lib.mkDefault true;

                  # !!! Welp, this is basically incomplete since we also have
                  # to consider user-specific specialArgs which is not set.
                  home-manager.extraSpecialArgs =
                    partsConfig.setups.home-manager.sharedSpecialArgs;
                }))

                (lib.mkIf (hasHMBranch && hasHomeManagerUsers)
                  ({ lib, pkgs, ... }: {
                    config = lib.mkMerge [
                      {
                        users.users =
                          lib.mapAttrs (name: hmUser: hmUser.userConfig)
                          setupConfig.home-manager.users;

                        home-manager.users = lib.mapAttrs (name: hmUser: {
                          imports =
                            partsConfig.setups.home-manager.configs.${name}.modules
                            ++ hmUser.additionalModules;
                        }) setupConfig.home-manager.users;
                      }

                      (lib.mkIf (isNixpkgs "global") {
                        home-manager.useGlobalPkgs = lib.mkForce true;

                        # Disable all options that are going to be blocked once
                        # `home-manager.useGlobalPkgs` is used.
                        home-manager.users = lib.mapAttrs (name: _: {
                          nixpkgs.overlays = lib.mkForce null;
                          nixpkgs.config = lib.mkForce null;
                        }) setupConfig.home-manager.users;

                        # Then apply all of the user overlays into the nixpkgs instance
                        # of the NixOS system.
                        nixpkgs.overlays = let
                          hmUsersOverlays = lib.mapAttrsToList (name: _:
                            partsConfig.setups.home-manager.configs.${name}.nixpkgs.overlays)
                            setupConfig.home-manager.users;

                          overlays = lib.lists.flatten hmUsersOverlays;
                          # Most of the overlays are going to be imported from a
                          # variable anyways. This should massively reduce the step
                          # needed for nixpkgs to do its thing.
                          #
                          # Though, it becomes unpredictable due to the way how the
                          # overlay list is constructed. However, this is much more
                          # preferable than letting a massive list with duplicated
                          # overlays from different home-manager users to be applied.
                          #
                          # Anyways, all I'm saying is that this is a massive hack
                          # because it isn't correct.
                        in lib.lists.unique overlays;
                      })

                      (lib.mkIf (isNixpkgs "separate") {
                        home-manager.useGlobalPkgs = lib.mkForce false;
                        home-manager.users = lib.mapAttrs (name: _: {
                          nixpkgs.overlays =
                            partsConfig.setups.home-manager.configs.${name}.nixpkgs.overlays;
                        }) setupConfig.home-manager.users;
                      })
                    ];
                  }))
              ];
            };
          })
      ]);
  };

  config = lib.mkMerge [
    {
      setups.home-manager.sharedNixpkgsConfig = config.setups.sharedNixpkgsConfig;

      setups.home-manager.sharedSpecialArgs = config.setups.sharedSpecialArgs;
    }

    (lib.mkIf (cfg.configs != { }) {
      flake = let
        # A quick data structure we can pass through multiple build pipelines.
        pureHomeManagerConfigs = let
          generatePureConfigs = username: 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 (mkHome {
                inherit pkgs system;
                inherit (metadata) homeManagerBranch;
                extraModules = cfg.sharedModules ++ cfg.standaloneConfigModules
                  ++ metadata.modules ++ metadata.standaloneConfigModules;
              })) metadata.systems);
        in lib.mapAttrs generatePureConfigs cfg.configs;
      in {
        homeConfigurations = let
          renameSystems = name: system: config:
            lib.nameValuePair "${name}-${system}" config;
        in lib.concatMapAttrs
        (name: configs: lib.mapAttrs' (renameSystems name) configs)
        pureHomeManagerConfigs;

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

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