{
  config,
  lib,
  pkgs,
  ...
}:

let
  inherit (lib) mkOption types;

  cfg = config.news;

  entryModule = types.submodule (
    { config, ... }:
    {
      options = {
        id = mkOption {
          internal = true;
          type = types.str;
          description = ''
            A unique entry identifier. By default it is a base16
            formatted hash of the entry message.
          '';
        };

        time = mkOption {
          internal = true;
          type = types.str;
          example = "2017-07-10T21:55:04+00:00";
          description = ''
            News entry time stamp in ISO-8601 format. Must be in UTC
            (ending in '+00:00').
          '';
        };

        condition = mkOption {
          internal = true;
          default = true;
          description = "Whether the news entry should be active.";
        };

        message = mkOption {
          internal = true;
          type = types.str;
          description = "The news entry content.";
        };
      };

      config = {
        id = lib.mkDefault (builtins.hashString "sha256" config.message);
      };
    }
  );

  isNixFile = n: v: v == "regular" && lib.hasSuffix ".nix" n;
  isDirectory = _n: v: v == "directory";

  # Recursively collect all .nix files from a directory
  collectNixFiles =
    dir:
    let
      contents = builtins.readDir dir;
      files = lib.filterAttrs isNixFile contents;
      fileList = map (file: dir + "/${file}") (builtins.attrNames files);

      # Process subdirectories
      subdirs = lib.filterAttrs isDirectory contents;
      subdirFiles = lib.concatMap (subdir: collectNixFiles (dir + "/${subdir}")) (
        builtins.attrNames subdirs
      );
    in
    fileList ++ subdirFiles;

  newsFiles = collectNixFiles ./news;
  newsEntries = map (
    newsFile:
    let
      imported = import newsFile;
    in
    if builtins.isFunction imported then imported { inherit config lib pkgs; } else imported
  ) newsFiles;
in
{
  meta.maintainers = [ lib.maintainers.rycee ];

  options = {
    news = {
      display = mkOption {
        type = types.enum [
          "silent"
          "notify"
          "show"
        ];
        default = "notify";
        description = ''
          How unread and relevant news should be presented when
          running {command}`home-manager build` and
          {command}`home-manager switch`.

          The options are

          `silent`
          : Do not print anything during build or switch. The
            {command}`home-manager news` command still
            works for viewing the entries.

          `notify`
          : The number of unread and relevant news entries will be
            printed to standard output. The {command}`home-manager
            news` command can later be used to view the entries.

          `show`
          : A pager showing unread news entries is opened.
        '';
      };

      entries = mkOption {
        internal = true;
        type = types.listOf entryModule;
        default = [ ];
        description = "News entries.";
      };

      json = {
        output = mkOption {
          internal = true;
          type = types.package;
          description = "The generated JSON file package.";
        };
      };
    };
  };

  config = {
    news.json.output = pkgs.writeText "hm-news.json" (
      builtins.toJSON { inherit (cfg) display entries; }
    );

    # News entries are now loaded from individual files in the news directory
    news.entries = newsEntries;
  };
}
