class StatelessStreamReceiver implements StreamReceiver {

  @Override
  public InterledgerResponsePacket receiveMoney(
      final InterledgerPreparePacket preparePacket, final InterledgerAddress receiverAddress,
      final Denomination denomination
  ) {
    Objects.requireNonNull(preparePacket);
    Objects.requireNonNull(receiverAddress);

    // Will throw if there's an error...
    final SharedSecret spspSharedSecret = this.streamConnectionGenerator
        .deriveSecretFromAddress(serverSecretSupplier, preparePacket.getDestination());
    final SharedSecret streamSharedSecret = SharedSecret.of(spspSharedSecret.key());

    final StreamPacket streamPacket;
    try {
      if (preparePacket.getData().length == 0) {
        return InterledgerRejectPacket.builder()
            .code(InterledgerErrorCode.F06_UNEXPECTED_PAYMENT)
            .message("No STREAM packet bytes available to decrypt")
            .triggeredBy(receiverAddress)
            .build();
      }
      // Try to parse the STREAM data from the payload.
      final byte[] streamPacketBytes = streamEncryptionService.decrypt(streamSharedSecret, preparePacket.getData());
      streamPacket = streamCodecContext.read(StreamPacket.class, new ByteArrayInputStream(streamPacketBytes));
    } catch (Exception e) {
      logger.error(
          "Unable to decrypt packet. preparePacket={} receiverAddress={} error={}",
          preparePacket, receiverAddress, e
      );
      return InterledgerRejectPacket.builder()
          .code(InterledgerErrorCode.F06_UNEXPECTED_PAYMENT)
          .message("Could not decrypt data")
          .triggeredBy(receiverAddress)
          .build();
    }

    final Builder<StreamFrame> responseFrames = ImmutableList.builder();

    if (streamPacket.sequenceIsSafeForSingleSharedSecret()) {
      streamPacket.frames().stream()
          .filter(streamFrame -> streamFrame.streamFrameType() == StreamFrameType.StreamMoney)
          .map($ -> (StreamMoneyFrame) $)
          // Tell the sender the stream can handle lots of money
          .forEach(streamMoneyFrame -> responseFrames.add(StreamMoneyMaxFrame.builder()
              .streamId(streamMoneyFrame.streamId())
              .totalReceived(UnsignedLong.ZERO)
              .receiveMax(UnsignedLong.MAX_VALUE)
              .build()));

      // Add ConnectionNewAddress, but only if the sender sent one. This allows the sender to ask multiple times, if
      // desired.
      streamPacket.frames().stream()
          .filter(streamFrame -> streamFrame.streamFrameType() == StreamFrameType.ConnectionNewAddress)
          .findFirst()
          .map(streamFrame -> responseFrames.add(ConnectionAssetDetailsFrame.builder()
              .sourceDenomination(denomination)
              .build()));
    } else {
      logger.warn("This STREAM Connection's sequence {} was too high for safe encryption. CLOSING the stream!",
          streamPacket.sequence());
      // If the sequence it too high, we should close the Connection.
      responseFrames.add(ConnectionCloseFrame.builder()
          .errorCode(ErrorCodes.ProtocolViolation)
          .errorMessage("Sequence number was to too high for safe encryption")
          .build());
    }

    // Generate fulfillment using the shared secret that was pre-negotiated with the sender.
    final InterledgerFulfillment fulfillment = StreamUtils
        .generatedFulfillableFulfillment(streamSharedSecret, preparePacket.getData());
    final boolean isFulfillable = fulfillment.getCondition().equals(preparePacket.getExecutionCondition());

    // Return Fulfill or Reject Packet
    if (isFulfillable && is(preparePacket.getAmount()).greaterThanEqualTo(streamPacket.prepareAmount())) {
      final StreamPacket returnableStreamPacketResponse = StreamPacket.builder()
          .sequence(streamPacket.sequence())
          .interledgerPacketType(InterledgerPacketType.FULFILL)
          .prepareAmount(preparePacket.getAmount())
          .frames(responseFrames.build())
          .sharedSecret(streamSharedSecret)
          .build();

      logger.debug(
          "Fulfilling prepare packet. preparePacket={} fulfillment={} returnableStreamPacketResponse={}",
          preparePacket, fulfillment, returnableStreamPacketResponse
      );

      try {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        streamCodecContext.write(returnableStreamPacketResponse, baos);
        final byte[] returnableStreamPacketBytes = baos.toByteArray();
        final byte[] encryptedReturnableStreamPacketBytes = streamEncryptionService
            .encrypt(streamSharedSecret, returnableStreamPacketBytes);

        return InterledgerFulfillPacket.builder()
            .fulfillment(fulfillment)
            .data(encryptedReturnableStreamPacketBytes)
            .typedData(returnableStreamPacketResponse)
            .build();

      } catch (IOException e) {
        throw new StreamException(e.getMessage(), e);
      }
    } else {
      final StreamPacket returnableStreamPacketResponse = StreamPacket.builder()
          .sequence(streamPacket.sequence())
          .interledgerPacketType(InterledgerPacketType.REJECT)
          .prepareAmount(preparePacket.getAmount())
          .frames(responseFrames.build())
          .build();

      if (isFulfillable) {
        logger.debug("Packet is unfulfillable. preparePacket={}", preparePacket);
      } else if (is(preparePacket.getAmount()).lessThan(streamPacket.prepareAmount())) {
        logger.debug(
            "Received only: {} when we should have received at least: {}",
            preparePacket.getAmount(), streamPacket.prepareAmount()
        );
      }

      logger.debug(
          "Rejecting Prepare and including encrypted stream packet. preparePacket={} returnableStreamPacketResponse={}",
          preparePacket, returnableStreamPacketResponse
      );

      try {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        streamCodecContext.write(returnableStreamPacketResponse, baos);
        final byte[] returnableStreamPacketBytes = baos.toByteArray();
        final byte[] encryptedReturnableStreamPacketBytes = streamEncryptionService
            .encrypt(streamSharedSecret, returnableStreamPacketBytes);

        return InterledgerRejectPacket.builder()
            .code(InterledgerErrorCode.F99_APPLICATION_ERROR)
            .message("STREAM packet not fulfillable (prepare amount < stream packet amount)")
            .triggeredBy(receiverAddress)
            .data(encryptedReturnableStreamPacketBytes)
            .typedData(returnableStreamPacketResponse)
            .build();
      } catch (IOException e) {
        throw new StreamException(e.getMessage(), e);
      }
    }
  }

  public  StatelessStreamReceiver(
      final ServerSecretSupplier serverSecretSupplier, final StreamConnectionGenerator streamConnectionGenerator,
      final StreamEncryptionService streamEncryptionService, final CodecContext streamCodecContext
  );

}