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

with lib;

let
  eachBitcoind = filterAttrs (bitcoindName: cfg: cfg.enable) config.services.bitcoind;

  rpcUserOpts = { name, ... }: {
    options = {
      name = mkOption {
        type = types.str;
        example = "alice";
        description = ''
          Username for JSON-RPC connections.
        '';
      };
      passwordHMAC = mkOption {
        type = types.uniq (types.strMatching "[0-9a-f]+\\$[0-9a-f]{64}");
        example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
        description = ''
          Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
          format \<SALT-HEX\>$\<HMAC-HEX\>.

          Tool (Python script) for HMAC generation is available here:
          <https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py>
        '';
      };
    };
    config = {
      name = mkDefault name;
    };
  };

  bitcoindOpts = { config, lib, name, ...}: {
    options = {

      enable = mkEnableOption "Bitcoin daemon";

      package = mkPackageOption pkgs "bitcoind" { };

      configFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        example = "/var/lib/${name}/bitcoin.conf";
        description = "The configuration file path to supply bitcoind.";
      };

      extraConfig = mkOption {
        type = types.lines;
        default = "";
        example = ''
          par=16
          rpcthreads=16
          logips=1
        '';
        description = "Additional configurations to be appended to {file}`bitcoin.conf`.";
      };

      dataDir = mkOption {
        type = types.path;
        default = "/var/lib/bitcoind-${name}";
        description = "The data directory for bitcoind.";
      };

      user = mkOption {
        type = types.str;
        default = "bitcoind-${name}";
        description = "The user as which to run bitcoind.";
      };

      group = mkOption {
        type = types.str;
        default = config.user;
        description = "The group as which to run bitcoind.";
      };

      rpc = {
        port = mkOption {
          type = types.nullOr types.port;
          default = null;
          description = "Override the default port on which to listen for JSON-RPC connections.";
        };
        users = mkOption {
          default = {};
          example = literalExpression ''
            {
              alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
              bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
            }
          '';
          type = types.attrsOf (types.submodule rpcUserOpts);
          description = "RPC user information for JSON-RPC connections.";
        };
      };

      pidFile = mkOption {
        type = types.path;
        default = "${config.dataDir}/bitcoind.pid";
        description = "Location of bitcoind pid file.";
      };

      testnet = mkOption {
        type = types.bool;
        default = false;
        description = "Whether to use the testnet instead of mainnet.";
      };

      port = mkOption {
        type = types.nullOr types.port;
        default = null;
        description = "Override the default port on which to listen for connections.";
      };

      dbCache = mkOption {
        type = types.nullOr (types.ints.between 4 16384);
        default = null;
        example = 4000;
        description = "Override the default database cache size in MiB.";
      };

      prune = mkOption {
        type = types.nullOr (types.coercedTo
          (types.enum [ "disable" "manual" ])
          (x: if x == "disable" then 0 else 1)
          types.ints.unsigned
        );
        default = null;
        example = 10000;
        description = ''
          Reduce storage requirements by enabling pruning (deleting) of old
          blocks. This allows the pruneblockchain RPC to be called to delete
          specific blocks, and enables automatic pruning of old blocks if a
          target size in MiB is provided. This mode is incompatible with -txindex
          and -rescan. Warning: Reverting this setting requires re-downloading
          the entire blockchain. ("disable" = disable pruning blocks, "manual"
          = allow manual pruning via RPC, >=550 = automatically prune block files
          to stay under the specified target size in MiB).
        '';
      };

      extraCmdlineOptions = mkOption {
        type = types.listOf types.str;
        default = [];
        description = ''
          Extra command line options to pass to bitcoind.
          Run bitcoind --help to list all available options.
        '';
      };
    };
  };
in
{

  options = {
    services.bitcoind = mkOption {
      type = types.attrsOf (types.submodule bitcoindOpts);
      default = {};
      description = "Specification of one or more bitcoind instances.";
    };
  };

  config = mkIf (eachBitcoind != {}) {

    assertions = flatten (mapAttrsToList (bitcoindName: cfg: [
    {
      assertion = (cfg.prune != null) -> (builtins.elem cfg.prune [ "disable" "manual" 0 1 ] || (builtins.isInt cfg.prune && cfg.prune >= 550));
      message = ''
        If set, services.bitcoind.${bitcoindName}.prune has to be "disable", "manual", 0 , 1 or >= 550.
      '';
    }
    {
      assertion = (cfg.rpc.users != {}) -> (cfg.configFile == null);
      message = ''
        You cannot set both services.bitcoind.${bitcoindName}.rpc.users and services.bitcoind.${bitcoindName}.configFile
        as they are exclusive. RPC user setting would have no effect if custom configFile would be used.
      '';
    }
    ]) eachBitcoind);

    environment.systemPackages = flatten (mapAttrsToList (bitcoindName: cfg: [
      cfg.package
    ]) eachBitcoind);

    systemd.services = mapAttrs' (bitcoindName: cfg: (
      nameValuePair "bitcoind-${bitcoindName}" (
      let
        configFile = pkgs.writeText "bitcoin.conf" ''
          # If Testnet is enabled, we need to add [test] section
          # otherwise, some options (e.g.: custom RPC port) will not work
          ${optionalString cfg.testnet "[test]"}
          # RPC users
          ${concatMapStringsSep  "\n"
            (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
            (attrValues cfg.rpc.users)
          }
          # Extra config options (from bitcoind nixos service)
          ${cfg.extraConfig}
        '';
      in {
        description = "Bitcoin daemon";
        wants = [ "network-online.target" ];
        after = [ "network-online.target" ];
        wantedBy = [ "multi-user.target" ];
        serviceConfig = {
          User = cfg.user;
          Group = cfg.group;
          ExecStart = ''
            ${cfg.package}/bin/bitcoind \
            ${if (cfg.configFile != null) then
              "-conf=${cfg.configFile}"
            else
              "-conf=${configFile}"
            } \
            -datadir=${cfg.dataDir} \
            -pid=${cfg.pidFile} \
            ${optionalString cfg.testnet "-testnet"}\
            ${optionalString (cfg.port != null) "-port=${toString cfg.port}"}\
            ${optionalString (cfg.prune != null) "-prune=${toString cfg.prune}"}\
            ${optionalString (cfg.dbCache != null) "-dbcache=${toString cfg.dbCache}"}\
            ${optionalString (cfg.rpc.port != null) "-rpcport=${toString cfg.rpc.port}"}\
            ${toString cfg.extraCmdlineOptions}
          '';
          Restart = "on-failure";

          # Hardening measures
          PrivateTmp = "true";
          ProtectSystem = "full";
          NoNewPrivileges = "true";
          PrivateDevices = "true";
          MemoryDenyWriteExecute = "true";
        };
      }
    ))) eachBitcoind;

    systemd.tmpfiles.rules = flatten (mapAttrsToList (bitcoindName: cfg: [
      "d '${cfg.dataDir}' 0770 '${cfg.user}' '${cfg.group}' - -"
    ]) eachBitcoind);

    users.users = mapAttrs' (bitcoindName: cfg: (
      nameValuePair "bitcoind-${bitcoindName}" {
      name = cfg.user;
      group = cfg.group;
      description = "Bitcoin daemon user";
      home = cfg.dataDir;
      isSystemUser = true;
    })) eachBitcoind;

    users.groups = mapAttrs' (bitcoindName: cfg: (
      nameValuePair "${cfg.group}" { }
    )) eachBitcoind;

  };

  meta.maintainers = with maintainers; [ _1000101 ];

}
