{ flake, flakeInputs, home-manager, path, hostName, config, lib, pkgs, customUtils, ... }:

with lib;

let
  profileSet = customUtils.types.attrNameSet flake.nixosModules.systemProfiles;
  userProfileSet = customUtils.types.attrNameSet (zipAttrs (attrValues flake.nixosModules.userProfiles));
  hasSops = config.sops.secrets != {};
in {
  imports =
    [ flakeInputs.sops-nix.nixosModules.sops
      home-manager.nixosModules.home-manager
    ];

  options = {
    # See mkSystemProfile in ../flake.nix
    system.profiles = mkOption {
      type = profileSet;
      default = [];
      description = ''
        Set (list without duplicates) of ‘systemProfiles’ enabled for this host
      '';
    };

    users.users = mkOption {
      type = types.attrsOf (types.submodule {
        options.profiles = mkOption {
          type = userProfileSet;
          default = [];
          description = ''
            Set (list without duplicates) of ‘userProfiles’ enabled for this user
          '';
        };
      });
    };

    nixpkgs.externalConfig = mkOption {
      default = {};
      example = literalExpression
        ''
          { allowBroken = true; allowUnfree = true; }
        '';
      type = mkOptionType {
        name = "nixpkgs-config";
        description = "nixpkgs config";
        check = x:
          let traceXIfNot = c:
                if c x then true
                else traceSeqN 1 x false;
              isConfig = x:
                builtins.isAttrs x || isFunction x;
          in traceXIfNot isConfig;
        merge = args:
          let
            optCall = f: x:
              if isFunction f
              then f x
              else f;
            mergeConfig = lhs_: rhs_:
              let
                lhs = optCall lhs_ { inherit pkgs; };
                rhs = optCall rhs_ { inherit pkgs; };
              in
              recursiveUpdate lhs rhs //
              optionalAttrs (lhs ? packageOverrides) {
                packageOverrides = pkgs:
                  optCall lhs.packageOverrides pkgs //
                  optCall (attrByPath [ "packageOverrides" ] { } rhs) pkgs;
              } //
              optionalAttrs (lhs ? perlPackageOverrides) {
                perlPackageOverrides = pkgs:
                  optCall lhs.perlPackageOverrides pkgs //
                  optCall (attrByPath [ "perlPackageOverrides" ] { } rhs) pkgs;
              };
          in foldr (def: mergeConfig def.value) {};
      };
      description = mdDoc ''
        The configuration of the Nix Packages collection.  (For
        details, see the Nixpkgs documentation.)  It allows you to set
        package configuration options.

        Used to construct `nixpkgs.pkgs`.
      '';
    };

    nixpkgs.flakeInput = mkOption {
      type = types.enum (attrNames flakeInputs);
      default = if flakeInputs ? "nixpkgs-${hostName}" then "nixpkgs-${hostName}" else "nixpkgs";
      defaultText = literalExpression ''if flakeInputs ? "nixpkgs-''${hostName}" then "nixpkgs-''${hostName}" else "nixpkgs"'';
      internal = true;
    };
  };

  config = {
    networking.hostName = hostName;
    system.configurationRevision = mkIf (flake ? rev) flake.rev;

    nixpkgs.pkgs = import (flakeInputs.${config.nixpkgs.flakeInput}.outPath + "/pkgs/top-level") {
      overlays = attrValues flake.overlays;
      config = config.nixpkgs.externalConfig;
      localSystem = config.nixpkgs.system;
    };

    nix = {
      package = pkgs.nixUnstable;
      settings = {
        sandbox = true;
        allowed-users = [ "*" ];
        trusted-users = [ "root" "@wheel" ];

        experimental-features = ["nix-command" "flakes" "auto-allocate-uids" "cgroups"];
        auto-allocate-uids = true;
        use-cgroups = true;
        use-xdg-base-directories = true;

        flake-registry = "${flakeInputs.flake-registry}/flake-registry.json";
      };
      nixPath = [
        "nixpkgs=${pkgs.runCommand "nixpkgs" {} ''
          mkdir $out
          ln -s ${./nixpkgs.nix} $out/default.nix
          ln -s /run/nixpkgs/lib $out/lib
        ''}"
      ];
      registry =
        let override = { self = "nixos"; };
        in mapAttrs' (inpName: inpFlake: nameValuePair
          (override.${inpName} or inpName)
          { flake = inpFlake; } ) flakeInputs;
    };

    systemd.tmpfiles.rules = [
      "L+ /run/nixpkgs - - - - ${flakeInputs.nixpkgs.outPath}"
      "L+ /run/nixpkgs-overlays.nix - - - - ${pkgs.writeText "overlays.nix" ''
        with builtins;

        attrValues (import
          (
            let lock = fromJSON (readFile ${flake + "/flake.lock"}); in
            fetchTarball {
              url = "https://github.com/edolstra/flake-compat/archive/''${lock.nodes.flake-compat.locked.rev}.tar.gz";
              sha256 = lock.nodes.flake-compat.locked.narHash;
            }
          )
          { src = ${flake}; }
        ).defaultNix.overlays
      ''}"
    ];

    users.mutableUsers = false;

    # documentation.nixos.includeAllModules = true; # incompatible with home-manager (build fails)

    home-manager = {
      useGlobalPkgs = true; # Otherwise home-manager would only work impurely
      useUserPackages = false;
      backupFileExtension = "bak";
    };

    sops = mkIf hasSops {
      age = {
        keyFile = "/var/lib/sops-nix/key.txt";
        generateKey = false;
        sshKeyPaths = [];
      };
      gnupg = {
        home = null;
        sshKeyPaths = [];
      };
    };

    programs.git = {
      enable = true;
      lfs.enable = true;
    };
    environment.systemPackages = with pkgs; [ git-annex scutiger ];

    system.activationScripts.symlink-flake = ''
      if test -L /etc/nixos; then
        ln -nsf ${flake} /etc/nixos
      elif test -d /etc/nixos && rmdir --ignore-fail-on-non-empty /etc/nixos; then
        ln -s ${flake} /etc/nixos
      fi
    '';
  };
}