"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageFactory = void 0;
const utils_1 = require("@streamr/utils");
const random_1 = __importDefault(require("lodash/random"));
const StreamMetadata_1 = require("../StreamMetadata");
const StreamrClientError_1 = require("../StreamrClientError");
const EncryptionUtil_1 = require("../encryption/EncryptionUtil");
const MessageID_1 = require("../protocol/MessageID");
const StreamMessage_1 = require("../protocol/StreamMessage");
const Mapping_1 = require("../utils/Mapping");
const utils_2 = require("../utils/utils");
const messageChain_1 = require("./messageChain");
class MessageFactory {
    streamId;
    authentication;
    defaultPartition;
    defaultMessageChainIds;
    prevMsgRefs = new Map();
    // eslint-disable-next-line max-len
    streamRegistry;
    groupKeyQueue;
    signatureValidator;
    messageSigner;
    firstMessage = true;
    constructor(opts) {
        this.streamId = opts.streamId;
        this.authentication = opts.authentication;
        this.streamRegistry = opts.streamRegistry;
        this.groupKeyQueue = opts.groupKeyQueue;
        this.signatureValidator = opts.signatureValidator;
        this.messageSigner = opts.messageSigner;
        this.defaultMessageChainIds = (0, Mapping_1.createLazyMap)({
            valueFactory: async () => {
                return (0, messageChain_1.createRandomMsgChainId)();
            }
        });
    }
    async createMessage(content, metadata, explicitPartition) {
        const publisherId = await this.getPublisherId(metadata);
        const isPublisher = await this.streamRegistry.isStreamPublisher(this.streamId, publisherId);
        if (!isPublisher) {
            this.streamRegistry.invalidatePermissionCaches(this.streamId);
            throw new StreamrClientError_1.StreamrClientError(`You don't have permission to publish to this stream. Using address: ${publisherId}`, 'MISSING_PERMISSION');
        }
        const streamMetadata = await this.streamRegistry.getStreamMetadata(this.streamId);
        const partitionCount = (0, StreamMetadata_1.getPartitionCount)(streamMetadata);
        let partition;
        if (explicitPartition !== undefined) {
            if ((explicitPartition < 0 || explicitPartition >= partitionCount)) {
                throw new Error(`Partition ${explicitPartition} is out of range (0..${partitionCount - 1})`);
            }
            if (metadata.partitionKey !== undefined) {
                throw new Error('Invalid combination of "partition" and "partitionKey"');
            }
            partition = explicitPartition;
        }
        else {
            partition = (metadata.partitionKey !== undefined)
                ? (0, utils_1.keyToArrayIndex)(partitionCount, metadata.partitionKey)
                : this.getDefaultPartition(partitionCount);
        }
        const msgChainId = metadata.msgChainId ?? await this.defaultMessageChainIds.get(partition);
        const msgChainKey = (0, utils_2.formLookupKey)([partition, msgChainId]);
        const prevMsgRef = this.prevMsgRefs.get(msgChainKey);
        const msgRef = (0, messageChain_1.createMessageRef)(metadata.timestamp, prevMsgRef);
        this.prevMsgRefs.set(msgChainKey, msgRef);
        const messageId = new MessageID_1.MessageID(this.streamId, partition, msgRef.timestamp, msgRef.sequenceNumber, publisherId, msgChainId);
        const encryptionType = (await this.streamRegistry.hasPublicSubscribePermission(this.streamId)) ? StreamMessage_1.EncryptionType.NONE : StreamMessage_1.EncryptionType.AES;
        let groupKeyId;
        let newGroupKey;
        let rawContent;
        let contentType;
        if (content instanceof Uint8Array) {
            contentType = StreamMessage_1.ContentType.BINARY;
            rawContent = content;
        }
        else {
            contentType = StreamMessage_1.ContentType.JSON;
            rawContent = (0, utils_1.utf8ToBinary)(JSON.stringify(content));
        }
        if (encryptionType === StreamMessage_1.EncryptionType.AES) {
            const keySequence = await this.groupKeyQueue.useGroupKey();
            rawContent = EncryptionUtil_1.EncryptionUtil.encryptWithAES(rawContent, keySequence.current.data);
            groupKeyId = keySequence.current.id;
            if (keySequence.next !== undefined) {
                newGroupKey = keySequence.current.encryptNextGroupKey(keySequence.next);
            }
        }
        const msg = await this.messageSigner.createSignedMessage({
            messageId,
            messageType: StreamMessage_1.StreamMessageType.MESSAGE,
            content: rawContent,
            prevMsgRef,
            encryptionType,
            groupKeyId,
            newGroupKey,
            contentType
        }, metadata.erc1271Contract !== undefined ? StreamMessage_1.SignatureType.ERC_1271 : StreamMessage_1.SignatureType.SECP256K1);
        // Assert the signature is valid for the first message. This is done here to improve user experience
        // in case the client signer is not authorized for the ERC-1271 contract.
        if (this.firstMessage) {
            this.firstMessage = false;
            if (metadata.erc1271Contract !== undefined) {
                await this.signatureValidator.assertSignatureIsValid(msg);
            }
        }
        return msg;
    }
    async getPublisherId(metadata) {
        if (metadata.erc1271Contract !== undefined) {
            // calling also toEthereumAddress() as it has stricter input validation than toUserId()
            return (0, utils_1.toUserId)((0, utils_1.toEthereumAddress)(metadata.erc1271Contract));
        }
        else {
            return this.authentication.getUserId();
        }
    }
    getDefaultPartition(partitionCount) {
        // we want to (re-)select a random partition in these two situations
        // 1) this is the first publish, and we have not yet selected any partition (the most typical case)
        // 2) the partition count may have decreased since we initially selected a random partitions, and it
        //    is now out-of-range (very rare case)
        if ((this.defaultPartition === undefined) || (this.defaultPartition >= partitionCount)) {
            this.defaultPartition = (0, random_1.default)(partitionCount - 1);
        }
        return this.defaultPartition;
    }
}
exports.MessageFactory = MessageFactory;
//# sourceMappingURL=MessageFactory.js.map