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

with lib;
let
  options = {
    services.liquidd = {
      enable = mkEnableOption "Liquid Bitcoin sidechain daemon";
      address = mkOption {
        type = types.str;
        default = "127.0.0.1";
        description = "Address to listen for peer connections.";
      };
      port = mkOption {
        type = types.port;
        default = 7042;
        description = "Override the default port on which to listen for connections.";
      };
      onionPort = mkOption {
        type = types.nullOr types.port;
        # When the liquidd onion service is enabled, add an onion-tagged socket
        # to distinguish local connections from Tor connections
        default = if (config.nix-bitcoin.onionServices.liquidd.enable or false) then 7043 else null;
        description = ''
          Port to listen for Tor peer connections.
          If set, inbound connections to this port are tagged as onion peers.
        '';
      };
      listen = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Listen for peer connections at `address:port`
          and `address:onionPort` (if `onionPort` is set).
        '';
      };
      listenWhitelisted = mkOption {
        type = types.bool;
        default = false;
        description = ''
          Listen for peer connections at `address:whitelistedPort`.
          Peers connected through this socket are automatically whitelisted.
        '';
      };
      whitelistedPort = mkOption {
        type = types.port;
        default = 7044;
        description = "See `listenWhitelisted`.";
      };
      extraConfig = mkOption {
        type = types.lines;
        default = "";
        example = ''
          par=16
          rpcthreads=16
          logips=1
        '';
        description = "Extra lines appended to <filename>elements.conf</filename>.";
      };
      dataDir = mkOption {
        type = types.path;
        default = "/var/lib/liquidd";
        description = "The data directory for liquidd.";
      };
      rpc = {
        address = mkOption {
          type = types.str;
          default = "127.0.0.1";
          description = "Address to listen for JSON-RPC connections.";
        };
        port = mkOption {
          type = types.port;
          default = 7041;
          description = "Port to listen for JSON-RPC connections.";
        };
        users = mkOption {
          default = {};
          example = {
            alice.passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
            bob.passwordHMAC = "b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99";
          };
          type = with types; attrsOf (submodule rpcUserOpts);
          description = ''
            RPC user information for JSON-RPC connections.
          '';
        };
      };
      rpcallowip = mkOption {
        type = types.listOf types.str;
        default = [ "127.0.0.1" ];
        description = ''
          Allow JSON-RPC connections from specified source.
        '';
      };
      rpcuser = mkOption {
          type = types.str;
          default = "liquidrpc";
          description = "Username for JSON-RPC connections";
      };
      proxy = mkOption {
        type = types.nullOr types.str;
        default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
        description = "Connect through SOCKS5 proxy";
      };
      dbCache = mkOption {
        type = types.nullOr (types.ints.between 4 16384);
        default = null;
        example = 4000;
        description = "Override the default database cache size in megabytes.";
      };
      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)
        '';
      };
      validatepegin = mkOption {
        type = types.nullOr types.bool;
        default = null;
        description = ''
          Validate pegin claims. All functionaries must run this.
        '';
      };
      user = mkOption {
        type = types.str;
        default = "liquid";
        description = "The user as which to run liquidd.";
      };
      group = mkOption {
        type = types.str;
        default = cfg.user;
        description = "The group as which to run liquidd.";
      };
      cli = mkOption {
        readOnly = true;
        default = pkgs.writeScriptBin "elements-cli" ''
          ${nbPkgs.elementsd}/bin/elements-cli -datadir='${cfg.dataDir}' "$@"
        '';
        defaultText = "(See source)";
        description = "Binary to connect with the liquidd instance.";
      };
      swapCli = mkOption {
        default = pkgs.writeScriptBin "liquidswap-cli" ''
          ${nbPkgs.liquid-swap}/bin/liquidswap-cli -c '${cfg.dataDir}/elements.conf' "$@"
        '';
        defaultText = "(See source)";
        description = "Binary for managing liquid swaps.";
      };
      tor = nbLib.tor;
    };
  };

  cfg = config.services.liquidd;
  nbLib = config.nix-bitcoin.lib;
  nbPkgs = config.nix-bitcoin.pkgs;
  secretsDir = config.nix-bitcoin.secretsDir;

  bitcoind = config.services.bitcoind;

  configFile = pkgs.writeText "elements.conf" ''
    # We're already logging via journald
    nodebuglogfile=1

    startupnotify=/run/current-system/systemd/bin/systemd-notify --ready

    chain=${bitcoind.makeNetworkName "liquidv1" ''
      regtest
      [regtest]'' # Add [regtest] config section
    }
    ${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
    ${optionalString (cfg.prune != null) "prune=${toString cfg.prune}"}
    ${optionalString (cfg.validatepegin != null) "validatepegin=${if cfg.validatepegin then "1" else "0"}"}

    # Connection options
    listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"}
    ${optionalString cfg.listen
      "bind=${cfg.address}:${toString cfg.port}"}
    ${optionalString (cfg.listen && cfg.onionPort != null)
      "bind=${cfg.address}:${toString cfg.onionPort}=onion"}
    ${optionalString cfg.listenWhitelisted
      "whitebind=${cfg.address}:${toString cfg.whitelistedPort}"}
    ${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}

    # RPC server options
    rpcport=${toString cfg.rpc.port}
    ${concatMapStringsSep  "\n"
      (rpcUser: "rpcauth=${rpcUser.name}:${rpcUser.passwordHMAC}")
      (attrValues cfg.rpc.users)
    }
    rpcbind=${cfg.rpc.address}
    rpcconnect=${cfg.rpc.address}
    ${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpcallowip}
    rpcuser=${cfg.rpcuser}
    mainchainrpchost=${nbLib.address bitcoind.rpc.address}
    mainchainrpcport=${toString bitcoind.rpc.port}
    mainchainrpcuser=${bitcoind.rpc.users.public.name}

    # Extra config options (from liquidd nixos service)
    ${cfg.extraConfig}
  '';
  rpcUserOpts = { name, ... }: {
    options = {
      name = mkOption {
        type = types.str;
        example = "alice";
        description = ''
          Username for JSON-RPC connections.
        '';
      };
      passwordHMAC = mkOption {
        type = with types; uniq (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`.
        '';
      };
    };
    config = {
      name = mkDefault name;
    };
  };
in {
  inherit options;

  config = mkIf cfg.enable {
    assertions = [
      { assertion = bitcoind.regtest -> cfg.validatepegin != true;
        message = "liquidd: `validatepegin` is incompatible with regtest.";
      }
    ];

    services.bitcoind.enable = true;

    environment.systemPackages = [
      nbPkgs.elementsd
      (hiPrio cfg.cli)
      (hiPrio cfg.swapCli)
    ];

    systemd.tmpfiles.rules = [
      "d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
    ];

    systemd.services.liquidd = {
      requires = [ "bitcoind.service" ];
      after = [ "bitcoind.service" ];
      wantedBy = [ "multi-user.target" ];
      preStart = ''
        install -m 640 ${configFile} '${cfg.dataDir}/elements.conf'
        {
          echo "rpcpassword=$(cat ${secretsDir}/liquid-rpcpassword)"
          echo "mainchainrpcpassword=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
        } >> '${cfg.dataDir}/elements.conf'
      '';
      serviceConfig = nbLib.defaultHardening // {
        Type = "notify";
        NotifyAccess = "all";
        User = cfg.user;
        Group = cfg.group;
        ExecStart = "${nbPkgs.elementsd}/bin/elementsd -datadir='${cfg.dataDir}'";
        Restart = "on-failure";
        ReadWritePaths = cfg.dataDir;
      } // nbLib.allowedIPAddresses cfg.tor.enforce;
    };

    users.users.${cfg.user} = {
      isSystemUser = true;
      group = cfg.group;
      extraGroups = [ "bitcoinrpc-public" ];
    };
    users.groups.${cfg.group} = {};
    nix-bitcoin.operator.groups = [ cfg.group ];

    nix-bitcoin.secrets.liquid-rpcpassword.user = cfg.user;
    nix-bitcoin.generateSecretsCmds.liquid = ''
      makePasswordSecret liquid-rpcpassword
    '';
  };
}