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

with lib;

let
  cfg = config.services.deluge;
  cfg_web = config.services.deluge.web;
  isDeluge1 = versionOlder cfg.package.version "2.0.0";

  openFilesLimit = 4096;
  listenPortsDefault = [ 6881 6889 ];

  listToRange = x: { from = elemAt x 0; to = elemAt x 1; };

  configDir = "${cfg.dataDir}/.config/deluge";
  configFile = pkgs.writeText "core.conf" (builtins.toJSON cfg.config);
  declarativeLockFile = "${configDir}/.declarative";

  preStart = if cfg.declarative then ''
    if [ -e ${declarativeLockFile} ]; then
      # Was declarative before, no need to back up anything
      ${if isDeluge1 then "ln -sf" else "cp"} ${configFile} ${configDir}/core.conf
      ln -sf ${cfg.authFile} ${configDir}/auth
    else
      # Declarative for the first time, backup stateful files
      ${if isDeluge1 then "ln -s" else "cp"} -b --suffix=.stateful ${configFile} ${configDir}/core.conf
      ln -sb --suffix=.stateful ${cfg.authFile} ${configDir}/auth
      echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
        > ${declarativeLockFile}
    fi
  '' else ''
    if [ -e ${declarativeLockFile} ]; then
      rm ${declarativeLockFile}
    fi
  '';
in {
  options = {
    services = {
      deluge = {
        enable = mkEnableOption "Deluge daemon";

        openFilesLimit = mkOption {
          default = openFilesLimit;
          type = types.either types.int types.str;
          description = ''
            Number of files to allow deluged to open.
          '';
        };

        config = mkOption {
          type = types.attrs;
          default = {};
          example = literalExpression ''
            {
              download_location = "/srv/torrents/";
              max_upload_speed = "1000.0";
              share_ratio_limit = "2.0";
              allow_remote = true;
              daemon_port = 58846;
              listen_ports = [ ${toString listenPortsDefault} ];
            }
          '';
          description = ''
            Deluge core configuration for the core.conf file. Only has an effect
            when {option}`services.deluge.declarative` is set to
            `true`. String values must be quoted, integer and
            boolean values must not. See
            <https://git.deluge-torrent.org/deluge/tree/deluge/core/preferencesmanager.py#n41>
            for the available options.
          '';
        };

        declarative = mkOption {
          type = types.bool;
          default = false;
          description = ''
            Whether to use a declarative deluge configuration.
            Only if set to `true`, the options
            {option}`services.deluge.config`,
            {option}`services.deluge.openFirewall` and
            {option}`services.deluge.authFile` will be
            applied.
          '';
        };

        openFirewall = mkOption {
          default = false;
          type = types.bool;
          description = ''
            Whether to open the firewall for the ports in
            {option}`services.deluge.config.listen_ports`. It only takes effet if
            {option}`services.deluge.declarative` is set to
            `true`.

            It does NOT apply to the daemon port nor the web UI port. To access those
            ports securely check the documentation
            <https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient#CreateSSHTunnel>
            or use a VPN or configure certificates for deluge.
          '';
        };

        dataDir = mkOption {
          type = types.path;
          default = "/var/lib/deluge";
          description = ''
            The directory where deluge will create files.
          '';
        };

        authFile = mkOption {
          type = types.path;
          example = "/run/keys/deluge-auth";
          description = ''
            The file managing the authentication for deluge, the format of this
            file is straightforward, each line contains a
            username:password:level tuple in plaintext. It only has an effect
            when {option}`services.deluge.declarative` is set to
            `true`.
            See <https://dev.deluge-torrent.org/wiki/UserGuide/Authentication> for
            more information.
          '';
        };

        user = mkOption {
          type = types.str;
          default = "deluge";
          description = ''
            User account under which deluge runs.
          '';
        };

        group = mkOption {
          type = types.str;
          default = "deluge";
          description = ''
            Group under which deluge runs.
          '';
        };

        extraPackages = mkOption {
          type = types.listOf types.package;
          default = [];
          description = ''
            Extra packages available at runtime to enable Deluge's plugins. For example,
            extraction utilities are required for the built-in "Extractor" plugin.
            This always contains unzip, gnutar, xz and bzip2.
          '';
        };

        package = mkPackageOption pkgs "deluge-2_x" { };
      };

      deluge.web = {
        enable = mkEnableOption "Deluge Web daemon";

        port = mkOption {
          type = types.port;
          default = 8112;
          description = ''
            Deluge web UI port.
          '';
        };

        openFirewall = mkOption {
          type = types.bool;
          default = false;
          description = ''
            Open ports in the firewall for deluge web daemon
          '';
        };
      };
    };
  };

  config = mkIf cfg.enable {

    services.deluge.package = mkDefault (
      if versionAtLeast config.system.stateVersion "20.09" then
        pkgs.deluge-2_x
      else
        # deluge-1_x is no longer packaged and this will resolve to an error
        # thanks to the alias for this name.  This is left here so that anyone
        # using NixOS older than 20.09 receives that error when they upgrade
        # and is forced to make an intentional choice to switch to deluge-2_x.
        # That might be slightly inconvenient but there is no path to
        # downgrade from 2.x to 1.x so NixOS should not automatically perform
        # this state migration.
        pkgs.deluge-1_x
    );

    # Provide a default set of `extraPackages`.
    services.deluge.extraPackages = with pkgs; [ unzip gnutar xz bzip2 ];

    systemd.tmpfiles.settings."10-deluged" = let
      defaultConfig = {
        inherit (cfg) user group;
        mode = "0770";
      };
    in {
      "${cfg.dataDir}".d = defaultConfig;
      "${cfg.dataDir}/.config".d = defaultConfig;
      "${cfg.dataDir}/.config/deluge".d = defaultConfig;
    }
    // optionalAttrs (cfg.config ? download_location) {
      ${cfg.config.download_location}.d = defaultConfig;
    }
    // optionalAttrs (cfg.config ? torrentfiles_location) {
      ${cfg.config.torrentfiles_location}.d = defaultConfig;
    }
    // optionalAttrs (cfg.config ? move_completed_path) {
      ${cfg.config.move_completed_path}.d = defaultConfig;
    };

    systemd.services.deluged = {
      after = [ "network.target" ];
      description = "Deluge BitTorrent Daemon";
      wantedBy = [ "multi-user.target" ];
      path = [ cfg.package ] ++ cfg.extraPackages;
      serviceConfig = {
        ExecStart = ''
          ${cfg.package}/bin/deluged \
            --do-not-daemonize \
            --config ${configDir}
        '';
        # To prevent "Quit & shutdown daemon" from working; we want systemd to
        # manage it!
        Restart = "on-success";
        User = cfg.user;
        Group = cfg.group;
        UMask = "0002";
        LimitNOFILE = cfg.openFilesLimit;
      };
      preStart = preStart;
    };

    systemd.services.delugeweb = mkIf cfg_web.enable {
      after = [ "network.target" "deluged.service"];
      requires = [ "deluged.service" ];
      description = "Deluge BitTorrent WebUI";
      wantedBy = [ "multi-user.target" ];
      path = [ cfg.package ];
      serviceConfig = {
        ExecStart = ''
          ${cfg.package}/bin/deluge-web \
            ${optionalString (!isDeluge1) "--do-not-daemonize"} \
            --config ${configDir} \
            --port ${toString cfg.web.port}
        '';
        User = cfg.user;
        Group = cfg.group;
      };
    };

    networking.firewall = mkMerge [
      (mkIf (cfg.declarative && cfg.openFirewall && !(cfg.config.random_port or true)) {
        allowedTCPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
        allowedUDPPortRanges = singleton (listToRange (cfg.config.listen_ports or listenPortsDefault));
      })
      (mkIf (cfg.web.openFirewall) {
        allowedTCPPorts = [ cfg.web.port ];
      })
    ];

    environment.systemPackages = [ cfg.package ];

    users.users = mkIf (cfg.user == "deluge") {
      deluge = {
        group = cfg.group;
        uid = config.ids.uids.deluge;
        home = cfg.dataDir;
        description = "Deluge Daemon user";
      };
    };

    users.groups = mkIf (cfg.group == "deluge") {
      deluge = {
        gid = config.ids.gids.deluge;
      };
    };
  };
}
