{
  inputs,
  lib,
  config,
  ...
}:

# ============================================================================
# Fleet Generator
# ============================================================================
# This file contains the logic to generate NixOS configurations for all hosts
# defined in inventory.nix. It supports both hostname-based and count-based
# configurations with flexible type associations.

let

  # Evaluate inventory to get fleet data
  # Import fleet-option.nix (defines athenix.fleet) and inventory.nix (sets values)
  # We use a minimal module here to avoid circular dependencies from common.nix's imports

  hostTypes = config.athenix.hwTypes;

  # Helper to create a single NixOS system configuration
  mkHost =
    {
      hostName,
      system ? "x86_64-linux",
      hostType,
      configOverrides ? { },
      externalModuleThunk ? null,
    }:
    let
      # Lazy evaluation: only fetch external module when building this host
      externalModulePath =
        if externalModuleThunk != null then
          let
            # Force evaluation of the thunk (fetchGit, fetchTarball, etc.)
            fetchedPath = externalModuleThunk;
            # Extract outPath from fetchGit/fetchTarball results
            extractedPath =
              if builtins.isAttrs fetchedPath && fetchedPath ? outPath then fetchedPath.outPath else fetchedPath;
          in
          if builtins.isPath extractedPath then
            extractedPath + "/default.nix"
          else if lib.isDerivation extractedPath then
            extractedPath + "/default.nix"
          else
            extractedPath + "/default.nix"
        else
          null;

      # Load users.nix to find external user modules
      accounts = config.athenix.users or { };

      # Build a map of user names to their nixos module paths (if they exist)
      # We'll use this to conditionally import modules based on user.enable
      userNixosModulePaths = lib.filterAttrs (_: v: v != null) (
        lib.mapAttrs (
          name: user:
          if (user ? external && user.external != null) then
            let
              externalPath =
                if builtins.isAttrs user.external && user.external ? outPath then
                  user.external.outPath
                else
                  user.external;
              nixosModulePath = externalPath + "/nixos.nix";
            in
            if
              (builtins.isPath externalPath || (builtins.isString externalPath && lib.hasPrefix "/" externalPath))
              && builtins.pathExists nixosModulePath
            then
              nixosModulePath
            else
              null
          else
            null
        ) accounts
      );

      # Create conditional wrapper modules for each user's nixos.nix
      # Each wrapper checks if the user is enabled before applying the module content
      userNixosModules = lib.mapAttrsToList (
        name: modulePath:
        {
          config,
          lib,
          pkgs,
          ...
        }@args:
        let
          # Import the user's nixos module - it returns a function or attrset
          importedModuleFunc = import modulePath { inherit inputs; };
          # If it's a function, call it with the module args; otherwise use as-is
          importedModule =
            if lib.isFunction importedModuleFunc then importedModuleFunc args else importedModuleFunc;
        in
        {
          config = lib.mkIf (config.athenix.users.${name}.enable or false) importedModule;
        }
      ) userNixosModulePaths;

      # Get the host type module from the hostTypes attribute set
      typeModule =
        hostTypes.${hostType}
          or (throw "Host type '${hostType}' not found. Available types: ${lib.concatStringsSep ", " (lib.attrNames hostTypes)}");

      # External module from fetchGit/fetchurl
      externalPathModule =
        if externalModulePath != null then import externalModulePath { inherit inputs; } else { };

      # Config override module - translate special keys to athenix options
      overrideModule =
        { ... }:
        let
          cleanConfig = lib.removeAttrs configOverrides [
            "type"
            "count"
            "devices"
            "overrides"
            "defaultCount"
            "buildMethods"
          ];
          specialConfig = lib.optionalAttrs (configOverrides ? buildMethods) {
            athenix.host.buildMethods = configOverrides.buildMethods;
          };
        in
        {
          config = lib.mkMerge [
            cleanConfig
            specialConfig
          ];
        };

      allModules =
        userNixosModules
        ++ [
          ./common.nix
          typeModule
          overrideModule
          { networking.hostName = hostName; }
        ]
        ++ lib.optional (externalModulePath != null) externalPathModule;
    in
    {
      system = lib.nixosSystem {
        inherit system;
        specialArgs = { inherit inputs; };
        modules = allModules;
      };
      modules = allModules;
    };

  # Process inventory entries - top-level keys are always prefixes
  processInventory = lib.mapAttrs (
    prefix: config:
    let
      hostType = config.type or prefix;
      system = config.system or "x86_64-linux";
      devices = config.devices or { };
      hasCount = config ? count;

      # Helper to generate hostname from prefix and suffix
      # Numbers get no dash: "nix-surface1", "nix-surface2"
      # Letters get dash: "nix-surface-alpha", "nix-surface-beta"
      mkHostName =
        prefix: suffix: usePrefix:
        if !usePrefix then
          suffix
        else if lib.match "[0-9]+" suffix != null then
          "${prefix}${suffix}" # numeric: no dash
        else
          "${prefix}-${suffix}"; # non-numeric: add dash

      # Extract common overrides and default count
      overrides = config.overrides or { };
      defaultCount = config.defaultCount or 0;

      # If devices is a number, treat it as count
      devicesValue = config.devices or { };
      actualDevices = if lib.isInt devicesValue then { } else devicesValue;
      actualCount = if lib.isInt devicesValue then devicesValue else (config.count or 0);

      # Clean base config - remove inventory management keys
      baseConfig = lib.removeAttrs config [
        "type"
        "system"
        "count"
        "devices"
        "overrides"
        "defaultCount"
      ];

      # Generate hosts from explicit device definitions
      deviceHosts = lib.listToAttrs (
        lib.mapAttrsToList (
          deviceKey: deviceConfig:
          let
            # Check if deviceConfig has an 'external' field for lazy evaluation
            hasExternalField = builtins.isAttrs deviceConfig && deviceConfig ? external;

            # Extract external module thunk if present (don't evaluate yet!)
            externalModuleThunk = if hasExternalField then deviceConfig.external else null;

            # Remove 'external' from config to avoid conflicts
            cleanDeviceConfig =
              if hasExternalField then lib.removeAttrs deviceConfig [ "external" ] else deviceConfig;

            # Merge: base config -> overrides -> device-specific config
            mergedConfig = lib.recursiveUpdate (lib.recursiveUpdate baseConfig overrides) cleanDeviceConfig;

            # Check useHostPrefix from the merged config
            usePrefix = mergedConfig.athenix.host.useHostPrefix or true;
            hostName = mkHostName prefix deviceKey usePrefix;
          in
          {
            name = hostName;
            value = mkHost {
              inherit
                hostName
                system
                hostType
                externalModuleThunk
                ;
              configOverrides = mergedConfig;
            };
          }
        ) actualDevices
      );

      # Generate numbered hosts from count or defaultCount
      # If devices are specified, defaultCount fills in the gaps
      countToUse = if actualCount > 0 then actualCount else defaultCount;

      # Get which numbered keys are already defined in devices
      existingNumbers = lib.filter (k: lib.match "[0-9]+" k != null) (lib.attrNames actualDevices);

      countHosts =
        if countToUse > 0 then
          lib.listToAttrs (
            lib.filter (x: x != null) (
              map (
                i:
                let
                  deviceKey = toString i;
                  # Skip if this number is already in explicit devices
                  alreadyDefined = lib.elem deviceKey existingNumbers;
                in
                if alreadyDefined then
                  null
                else
                  let
                    hostName = mkHostName prefix deviceKey true;
                    mergedConfig = lib.recursiveUpdate baseConfig overrides;
                  in
                  {
                    name = hostName;
                    value = mkHost {
                      inherit hostName system hostType;
                      configOverrides = mergedConfig;
                    };
                  }
              ) (lib.range 1 countToUse)
            )
          )
        else
          { };
    in
    lib.recursiveUpdate deviceHosts countHosts
  );

  fleetData = config.athenix.fleet;

  # Flatten the nested structure
  allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues (processInventory fleetData));
in
{
  nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts;
  modules = lib.mapAttrs (n: v: v.modules) allHosts;
}
