{
  pkgs,
  config,
  options,
  lib,
  ...
}:
let
  inherit (lib) types mkOption mkPackageOption;
  inherit (lib) optional;
  builders = lib.nixvim.builders.withPkgs pkgs;
  inherit (pkgs.stdenv.hostPlatform) system;
in
{
  options = {
    autowrapRuntimeDeps = mkOption {
      type = types.bool;
      default = true;
      description = ''
        Whether to automatically add plugin runtime dependencies to the Neovim wrapper's `PATH`.
      '';
    };

    viAlias = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Symlink `vi` to `nvim` binary.
      '';
    };

    vimAlias = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Symlink `vim` to `nvim` binary.
      '';
    };

    waylandSupport = mkOption {
      type = types.bool;
      default = lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.wayland;
      defaultText = lib.literalExpression "lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.wayland";
      description = "Enable additional wayland support, such as wl-clipboard for wayland clipboard syncing.";
    };

    withRuby = mkOption {
      type = types.bool;
      default = true;
      description = "Enable Ruby provider.";
    };

    withNodeJs = mkOption {
      type = types.bool;
      default = false;
      description = "Enable Node provider.";
    };

    withPerl = mkOption {
      type = types.bool;
      default = false;
      description = "Enable Perl provider.";
    };

    withPython3 = mkOption {
      type = types.bool;
      default = true;
      description = "Enable Python 3 provider.";
    };

    package = mkPackageOption pkgs "Neovim" {
      default = "neovim-unwrapped";
    };

    wrapRc = mkOption {
      type = types.bool;
      description = ''
        Whether the config will be included in the wrapper script.

        When enabled, the Nixvim config will be passed to `nvim` using the `VIMINIT` environment variable.
      '';
      defaultText = lib.literalMD ''
        Configured by your installation method: `false` when using the Home Manager module, `true` otherwise.
      '';
    };

    impureRtp = mkOption {
      type = types.bool;
      description = ''
        Whether to keep the (impure) nvim config directory in the runtimepath.

        If disabled, the XDG config dirs `nvim` and `nvim/after` will be removed from the runtimepath.
        When `wrapRc` is enabled, the wrapper will also ignore system/XDG config dirs during startup.
      '';
      defaultText = lib.literalMD ''
        Configured by your installation method: `true` when using the Home Manager module, `false` otherwise.
      '';
    };

    enablePrintInit = mkOption {
      type = types.bool;
      default = true;
      example = false;
      description = ''
        Install a tool that shows the content of the generated `init.lua` file.

        Run it using `${config.build.printInitPackage.meta.mainProgram}`.
      '';
    };

    build = {
      # TODO: `standalonePackage`; i.e. package + printInitPackage + man-docs bundled together

      package = mkOption {
        type = types.package;
        description = ''
          Wrapped Neovim.

          > [!NOTE]
          > Evaluating this option will also check `assertions` and print any `warnings`.
          > If this is not desired, you can use `build.packageUnchecked` instead.
        '';
        readOnly = true;
        defaultText = lib.literalExpression "config.build.packageUnchecked";
        apply =
          let
            assertions = builtins.concatMap (x: lib.optional (!x.assertion) x.message) config.assertions;
          in
          if assertions != [ ] then
            throw "\nFailed assertions:\n${lib.concatMapStringsSep "\n" (msg: "- ${msg}") assertions}"
          else
            lib.showWarnings config.warnings;
      };

      packageUnchecked = mkOption {
        type = types.package;
        description = ''
          Wrapped Neovim (without checking warnings or assertions).
        '';
        readOnly = true;
      };

      nvimPackage = mkOption {
        type = types.package;
        description = ''
          Wrapped Neovim (without man-docs, printInitPackage, etc).
        '';
        readOnly = true;
        internal = true;
      };

      initFile = mkOption {
        type = types.path;
        description = ''
          The generated `init.lua` file.

          > [!NOTE]
          > If `performance.byteCompileLua` is enabled, this file may not contain human-readable lua source code.
          > Consider using `build.initSource` instead.
        '';
        readOnly = true;
        visible = false;
      };

      initSource = mkOption {
        type = types.path;
        description = ''
          The generated `init.lua` source file.

          This is usually identical to `build.initFile`, however if `performance.byteCompileLua` is enabled,
          this option will refer to the un-compiled lua source file.
        '';
        readOnly = true;
        visible = false;
      };

      printInitPackage = mkOption {
        type = types.package;
        description = ''
          A tool to show the content of the generated `init.lua` file.
          Run using `${config.build.printInitPackage.meta.mainProgram}`.
        '';
        readOnly = true;
        visible = false;
      };

      manDocsPackage = mkOption {
        type = types.package;
        defaultText = lib.literalMD "`packages.<system>.man-docs` from Nixvim's flake";
        description = ''
          Nixvim's manpage documentation.
        '';
        readOnly = true;
        visible = false;
      };
    };
  };

  config =
    let
      # Optionally byte compile lua library
      inherit (pkgs.callPackage ./plugins/byte-compile-lua-lib.nix { inherit lib; })
        byteCompileLuaPackages
        ;
      extraLuaPackages =
        if config.performance.byteCompileLua.enable && config.performance.byteCompileLua.luaLib then
          ps: byteCompileLuaPackages (config.extraLuaPackages ps)
        else
          config.extraLuaPackages;

      vimPackageInfo = pkgs.neovimUtils.makeVimPackageInfo config.build.plugins;

      luaPackagesForWrapper =
        ps:
        extraLuaPackages ps
        ++ lib.optionals (vimPackageInfo ? luaDependencies) vimPackageInfo.luaDependencies;

      luaEnv = package.lua.withPackages luaPackagesForWrapper;

      wrappedNeovim =
        (pkgs.wrapNeovimUnstable package {
          extraLuaPackages = luaPackagesForWrapper;
          inherit (config)
            autowrapRuntimeDeps
            extraPython3Packages
            viAlias
            vimAlias
            waylandSupport
            withRuby
            withNodeJs
            withPerl
            withPython3
            ;
          inherit (config.build) plugins;
          wrapperArgs = lib.optionals (luaEnv != null) [
            "--prefix"
            "LUA_PATH"
            ";"
            (package.lua.pkgs.luaLib.genLuaPathAbsStr luaEnv)
            "--prefix"
            "LUA_CPATH"
            ";"
            (package.lua.pkgs.luaLib.genLuaCPathAbsStr luaEnv)
          ];
        }).overrideAttrs
          {
            # TODO: 2025-01-06
            # Wait for user feedback on disabling the fixup phase.
            # Ideally this will be upstreamed to nixpkgs.
            # See https://github.com/nix-community/nixvim/pull/3660#discussion_r2326250439
            dontFixup = true;
          };

      nvimPackage = wrappedNeovim.override (prev: {
        wrapperArgs =
          (if lib.isString prev.wrapperArgs then prev.wrapperArgs else lib.escapeShellArgs prev.wrapperArgs)
          + " "
          + extraWrapperArgs;
        wrapRc = false;
      });

      customRC = lib.nixvim.concatNonEmptyLines [
        (lib.nixvim.wrapVimscriptForLua wrappedNeovim.initRc)
        (nvimPackage.passthru.providerLuaRc or "")
        config.content
      ];

      initSource = builders.writeLua "init.lua" customRC;
      initByteCompiled = builders.writeByteCompiledLua "init.lua" customRC;
      initFile =
        if
          config.type == "lua"
          && config.performance.byteCompileLua.enable
          && config.performance.byteCompileLua.initLua
        then
          initByteCompiled
        else
          initSource;

      # `:help initialization` says sysinit is sourced before `VIMINIT` from
      # `$XDG_CONFIG_DIRS/nvim/sysinit.vim` or, as fallback, `$VIM/sysinit.vim`.
      impureStartupEnvVars = [
        "XDG_CONFIG_DIRS"
        "VIM"
      ];

      # Runs via `--cmd` before sysinit, so system/XDG config dirs are hidden
      # before Nvim checks them without relying on makeWrapper-only `--run`.
      saveAndClearImpureStartupEnvVars = ''
        _G.__nixvim_impure_startup_env = {}
        for _, name in ipairs(${lib.nixvim.toLuaObject impureStartupEnvVars}) do
          local value = vim.env[name]
          if value ~= nil then
            _G.__nixvim_impure_startup_env[name] = {
              set = true,
              value = value,
            }
          else
            _G.__nixvim_impure_startup_env[name] = {
              set = false,
            }
          end
          vim.env[name] = nil
        end
      '';

      # Restore before loading the generated config so user code sees the original
      # environment while sysinit remains skipped.
      restoreImpureStartupEnvVars = ''
        local saved_impure_startup_env = _G.__nixvim_impure_startup_env or {}
        for _, name in ipairs(${lib.nixvim.toLuaObject impureStartupEnvVars}) do
          local saved = saved_impure_startup_env[name]
          if saved ~= nil and saved.set then
            vim.env[name] = saved.value
          else
            vim.env[name] = nil
          end
        end
        _G.__nixvim_impure_startup_env = nil
      '';

      extraWrapperArgs = builtins.concatStringsSep " " (
        # Setting environment variables in the wrapper
        (lib.mapAttrsToList (
          name: value:
          lib.escapeShellArgs [
            "--set"
            name
            value
          ]
        ) config.env)
        ++ (optional (
          config.extraPackages != [ ]
        ) ''--prefix PATH : "${lib.makeBinPath config.extraPackages}"'')
        ++ (optional (
          config.extraPackagesAfter != [ ]
        ) ''--suffix PATH : "${lib.makeBinPath config.extraPackagesAfter}"'')
        ++ (optional (config.wrapRc && !config.impureRtp) (
          lib.escapeShellArgs [
            "--add-flag"
            "--cmd"
            "--add-flag"
            "lua ${saveAndClearImpureStartupEnvVars}"
          ]
        ))
        ++ (optional config.wrapRc (
          lib.escapeShellArgs [
            "--set"
            "VIMINIT"
            "lua ${lib.optionalString (!config.impureRtp) restoreImpureStartupEnvVars} dofile('${initFile}')"
          ]
        ))
      );

      package =
        if config.performance.byteCompileLua.enable && config.performance.byteCompileLua.nvimRuntime then
          # Using symlinkJoin to avoid rebuilding neovim
          pkgs.symlinkJoin {
            name = "neovim-byte-compiled-${lib.getVersion config.package}";
            paths = [ config.package ];
            # Required attributes from original neovim package
            inherit (config.package) lua meta;
            nativeBuildInputs = [ builders.byteCompileLuaHook ];
            postBuild = ''
              # Replace Nvim's binary symlink with a regular file,
              # or Nvim will use original runtime directory
              rm $out/bin/nvim
              cp ${config.package}/bin/nvim $out/bin/nvim

              runHook postFixup
            '';
          }
        else
          config.package;
    in
    {
      build = {
        inherit initFile initSource nvimPackage;
        package = config.build.packageUnchecked;

        packageUnchecked = pkgs.symlinkJoin {
          name = "nixvim";
          paths =
            with config.build;
            [
              nvimPackage
            ]
            ++ lib.optionals config.enableMan [
              manDocsPackage
            ]
            ++ lib.optionals config.enablePrintInit [
              printInitPackage
            ];
          meta.mainProgram = "nvim";
          passthru = {
            inherit config options;
          };
        };

        printInitPackage = pkgs.writeShellApplication {
          name = "nixvim-print-init";
          runtimeInputs = [ pkgs.bat ];
          runtimeEnv = {
            init = config.build.initSource;
          };
          text = ''
            bat --language=lua "$init"
          '';
        };

        # TODO: make `build.manDocsPackage` available in non-flake evals.
        # Also update `enableMan` default value.
        manDocsPackage =
          lib.throwIfNot options.flake.isDefined
            "`${options.build.manDocsPackage}` currently requires `${options.flake}`, which is not defined."
            config.flake.packages.${system}.man-docs;
      };

      # Set `wrapRc` and `impureRtp`s option defaults with even lower priority than `mkOptionDefault`
      wrapRc = lib.mkOverride 1501 true;
      impureRtp = lib.mkOverride 1501 false;

      assertions = lib.nixvim.mkAssertions "output" [
        {
          assertion = config.impureRtp || config.wrapRc;
          message = "`impureRtp = false` requires `wrapRc = true` so Nixvim can suppress system/XDG startup config.";
        }
      ];

      extraConfigLuaPre = lib.mkOrder 100 (
        lib.concatStringsSep "\n" (
          lib.optional (!config.impureRtp) ''
            -- Ignore the user lua configuration
            vim.opt.runtimepath:remove(vim.fn.stdpath('config'))              -- ~/.config/nvim
            vim.opt.runtimepath:remove(vim.fn.stdpath('config') .. "/after")  -- ~/.config/nvim/after
          ''
          # Add a global table at start of init
          ++ lib.singleton ''
            -- Nixvim's internal module table
            -- Can be used to share code throughout init.lua
            local _M = {}
          ''
        )
      );

      extraPlugins = lib.mkIf config.wrapRc [ config.build.extraFiles ];
    };
}
