{
  config,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.services.cross-seed;

  inherit (lib)
    mkEnableOption
    mkPackageOption
    mkOption
    types
    ;
  settingsFormat = pkgs.formats.json { };

  generatedConfig =
    pkgs.runCommand "cross-seed-gen-config" { nativeBuildInputs = [ pkgs.cross-seed ]; }
      ''
        export HOME=$(mktemp -d)
        cross-seed gen-config
        mkdir $out
        cp -r $HOME/.cross-seed/config.js $out/
      '';
in
{
  options.services.cross-seed = {
    enable = mkEnableOption "cross-seed";

    package = mkPackageOption pkgs "cross-seed" { };

    user = mkOption {
      type = types.str;
      default = "cross-seed";
      description = "User to run cross-seed as.";
    };

    group = mkOption {
      type = types.str;
      default = "cross-seed";
      example = "torrents";
      description = "Group to run cross-seed as.";
    };

    configDir = mkOption {
      type = types.path;
      default = "/var/lib/cross-seed";
      description = "Cross-seed config directory";
    };

    useGenConfigDefaults = mkOption {
      type = types.bool;
      default = false;
      description = ''
        Whether to use the option defaults from the configuration generated by
        {command}`cross-seed gen-config`.

        Those are the settings recommended by the project, and can be inspected
        from their [template file](https://github.com/cross-seed/cross-seed/blob/master/src/config.template.cjs).

        Settings set in {option}`services.cross-seed.settings` and
        {option}`services.cross-seed.settingsFile` will override the ones from
        this option.
      '';
    };

    settings = mkOption {
      default = { };
      type = types.submodule {
        freeformType = settingsFormat.type;
        options = {
          dataDirs = mkOption {
            type = types.listOf types.path;
            default = [ ];
            description = ''
              Paths to be searched for matching data.

              If you use Injection, cross-seed will use the specified linkType
              to create a link to the original file in the linkDirs.

              If linkType is hardlink, these must be on the same volume as the
              data.
            '';
          };

          linkDirs = mkOption {
            type = types.listOf types.path;
            default = [ ];
            description = ''
              List of directories where cross-seed will create links.

              If linkType is hardlink, these must be on the same volume as the data.
            '';
          };

          torrentDir = mkOption {
            type = types.nullOr types.path;
            default = null;
            description = ''
              Directory containing torrent files, or if you're using a torrent
              client integration and injection - your torrent client's .torrent
              file store/cache.
            '';
          };

          outputDir = mkOption {
            type = types.nullOr types.path;
            default = "${cfg.configDir}/output";
            defaultText = "\${cfg.configDir}/output";
            description = "Directory where cross-seed will place torrent files it finds.";
          };

          port = mkOption {
            type = types.port;
            default = 2468;
            example = 3000;
            description = "Port the cross-seed daemon listens on.";
          };
        };
      };

      description = ''
        Configuration options for cross-seed.

        Secrets should not be set in this option, as they will be available in
        the Nix store. For secrets, please use settingsFile.

        For more details, see [the cross-seed documentation](https://www.cross-seed.org/docs/basics/options).
      '';
    };

    settingsFile = lib.mkOption {
      default = null;
      type = types.nullOr types.path;
      description = ''
        Path to a JSON file containing settings that will be merged with the
        settings option. This is suitable for storing secrets, as they will not
        be exposed on the Nix store.
      '';
    };
  };

  config =
    let
      jsonSettingsFile = settingsFormat.generate "settings.json" cfg.settings;

      genConfigSegment =
        lib.optionalString cfg.useGenConfigDefaults # js
          ''
            const gen_config_js = "${generatedConfig}/config.js";
            Object.assign(loaded_settings, require(gen_config_js));
          '';

      # Since cross-seed uses a javascript config file, we can use node's
      # ability to parse JSON directly to avoid having to do any conversion.
      # This also means we don't need to use any external programs to merge the
      # secrets.
      secretSettingsSegment =
        lib.optionalString (cfg.settingsFile != null) # js
          ''
            const path = require("node:path");
            const secret_settings_json = path.join(process.env.CREDENTIALS_DIRECTORY, "secretSettingsFile");
            Object.assign(loaded_settings, JSON.parse(fs.readFileSync(secret_settings_json, "utf8")));
          '';

      javascriptConfig =
        pkgs.writeText "config.js" # js
          ''
            "use strict";
            const fs = require("fs");
            const settings_json = "${jsonSettingsFile}";
            let loaded_settings = {};
            ${genConfigSegment}
            Object.assign(loaded_settings, JSON.parse(fs.readFileSync(settings_json, "utf8")));
            ${secretSettingsSegment}
            module.exports = loaded_settings;
          '';
    in
    lib.mkIf (cfg.enable) {
      assertions = [
        {
          assertion = !(cfg.settings ? apiKey);
          message = ''
            The API key should be set via the settingsFile option, to avoid
            exposing it on the Nix store.
          '';
        }
      ];

      systemd.tmpfiles.settings."10-cross-seed" = {
        ${cfg.configDir}.d = {
          inherit (cfg) group user;
          mode = "700";
        };
      }
      // lib.optionalAttrs (cfg.settings.outputDir != null) {
        ${cfg.settings.outputDir}.d = {
          inherit (cfg) group user;
          mode = "750";
        };
      };

      systemd.services.cross-seed = {
        description = "cross-seed";
        after = [ "network-online.target" ];
        wants = [ "network-online.target" ];
        wantedBy = [ "multi-user.target" ];
        environment.CONFIG_DIR = cfg.configDir;
        preStart = ''
          install -D -m 600 -o '${cfg.user}' -g '${cfg.group}' '${javascriptConfig}' '${cfg.configDir}/config.js'
        '';

        serviceConfig = {
          ExecStart = "${lib.getExe cfg.package} daemon";
          User = cfg.user;
          Group = cfg.group;

          # Only allow binding to the specified port.
          SocketBindDeny = "any";
          SocketBindAllow = cfg.settings.port;

          LoadCredential = lib.mkIf (cfg.settingsFile != null) "secretSettingsFile:${cfg.settingsFile}";

          StateDirectory = "cross-seed";
          ReadWritePaths = lib.optional (cfg.settings.outputDir != null) cfg.settings.outputDir;
          ReadOnlyPaths = lib.optional (cfg.settings.torrentDir != null) cfg.settings.torrentDir;
        };

        unitConfig = {
          # Unfortunately, we can not protect these if we are to hardlink between them, as they need to be on the same volume for hardlinks to work.
          RequiresMountsFor = lib.flatten [
            cfg.settings.dataDirs
            cfg.settings.linkDirs
            (lib.optional (cfg.settings.outputDir != null) cfg.settings.outputDir)
          ];
        };
      };

      # It's useful to have the package in the path, to be able to e.g. get the API key.
      environment.systemPackages = [ cfg.package ];

      users.users = lib.mkIf (cfg.user == "cross-seed") {
        cross-seed = {
          group = cfg.group;
          description = "cross-seed user";
          isSystemUser = true;
          home = cfg.configDir;
        };
      };

      users.groups = lib.mkIf (cfg.group == "cross-seed") {
        cross-seed = { };
      };
    };
}
