var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { authenticate } from '../core/auth.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { StorageController } from '../store/storage-controller.js';
import { Cid, DataStream, DwnConstant, Encoder } from '../index.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js';
export class RecordsWriteHandler {
    constructor(didResolver, messageStore, dataStore, eventLog) {
        this.didResolver = didResolver;
        this.messageStore = messageStore;
        this.dataStore = dataStore;
        this.eventLog = eventLog;
    }
    handle({ tenant, message, options, dataStream }) {
        return __awaiter(this, void 0, void 0, function* () {
            let recordsWrite;
            try {
                recordsWrite = yield RecordsWrite.parse(message);
                // Protocol record specific validation
                if (message.descriptor.protocol !== undefined) {
                    yield ProtocolAuthorization.validateReferentialIntegrity(tenant, recordsWrite, this.messageStore);
                }
            }
            catch (e) {
                return messageReplyFromError(e, 400);
            }
            // authentication & authorization
            try {
                yield authenticate(message.authorization, this.didResolver);
                yield recordsWrite.authorize(tenant, this.messageStore);
            }
            catch (e) {
                return messageReplyFromError(e, 401);
            }
            // get existing messages matching the `recordId`
            const query = {
                interface: DwnInterfaceName.Records,
                recordId: message.recordId
            };
            const { messages: existingMessages } = yield this.messageStore.query(tenant, [query]);
            // if the incoming write is not the initial write, then it must not modify any immutable properties defined by the initial write
            const newMessageIsInitialWrite = yield recordsWrite.isInitialWrite();
            if (!newMessageIsInitialWrite) {
                try {
                    const initialWrite = yield RecordsWrite.getInitialWrite(existingMessages);
                    RecordsWrite.verifyEqualityOfImmutableProperties(initialWrite, message);
                }
                catch (e) {
                    return messageReplyFromError(e, 400);
                }
            }
            const newestExistingMessage = yield Message.getNewestMessage(existingMessages);
            let incomingMessageIsNewest = false;
            let newestMessage; // keep reference of newest message for pruning later
            if (newestExistingMessage === undefined || (yield Message.isNewer(message, newestExistingMessage))) {
                incomingMessageIsNewest = true;
                newestMessage = message;
            }
            else { // existing message is the same age or newer than the incoming message
                newestMessage = newestExistingMessage;
            }
            if (!incomingMessageIsNewest) {
                return {
                    status: { code: 409, detail: 'Conflict' }
                };
            }
            const isLatestBaseState = true;
            const indexes = yield constructRecordsWriteIndexes(recordsWrite, isLatestBaseState);
            // if data is below a certain threshold, we embed the data directly into the message for storage in MessageStore.
            let messageWithOptionalEncodedData = message;
            // try to store data, unless options explicitly say to skip storage
            if (options === undefined || !options.skipDataStorage) {
                if (dataStream === undefined && (newestExistingMessage === null || newestExistingMessage === void 0 ? void 0 : newestExistingMessage.descriptor.method) === DwnMethodName.Delete) {
                    return messageReplyFromError(new DwnError(DwnErrorCode.RecordsWriteMissingDataStream, 'No data stream was provided with the previous message being a delete'), 400);
                }
                try {
                    // if data is below the threshold, we store it within MessageStore
                    if (message.descriptor.dataSize <= DwnConstant.maxDataSizeAllowedToBeEncoded) {
                        // processes and sets `encodedData` with appropriate data.
                        messageWithOptionalEncodedData = yield this.processEncodedData(message, dataStream, newestExistingMessage);
                    }
                    else {
                        yield this.putData(tenant, message, dataStream);
                    }
                }
                catch (error) {
                    const e = error;
                    if (e.code === DwnErrorCode.RecordsWriteMissingDataInPrevious ||
                        e.code === DwnErrorCode.RecordsWriteMissingDataAssociation ||
                        e.code === DwnErrorCode.RecordsWriteDataCidMismatch ||
                        e.code === DwnErrorCode.RecordsWriteDataSizeMismatch) {
                        return messageReplyFromError(error, 400);
                    }
                    // else throw
                    throw error;
                }
            }
            yield this.messageStore.put(tenant, messageWithOptionalEncodedData, indexes);
            yield this.eventLog.append(tenant, yield Message.getCid(message));
            const messageReply = {
                status: { code: 202, detail: 'Accepted' }
            };
            // delete all existing messages that are not newest, except for the initial write
            yield StorageController.deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore, this.dataStore, this.eventLog);
            return messageReply;
        });
    }
    ;
    /**
     * Embeds the record's data into the `encodedData` property.
     * If dataStream is present, it uses the dataStream. Otherwise, uses the `encodedData` from the most recent RecordsWrite.
     *
     * @returns {RecordsWriteMessageWithOptionalEncodedData} `encodedData` embedded.
     *
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteMissingDataInPrevious`
     *                    if `dataStream` is absent AND `encodedData` of previous message is missing
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataCidMismatch`
     *                    if the data stream resulted in a data CID that mismatches with `dataCid` in the given message
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataSizeMismatch`
     *                    if `dataSize` in `descriptor` given mismatches the actual data size
     */
    processEncodedData(message, dataStream, newestExistingMessage) {
        return __awaiter(this, void 0, void 0, function* () {
            let dataBytes;
            if (dataStream === undefined) {
                const newestWithData = newestExistingMessage;
                if ((newestWithData === null || newestWithData === void 0 ? void 0 : newestWithData.encodedData) === undefined) {
                    throw new DwnError(DwnErrorCode.RecordsWriteMissingDataInPrevious, `No dataStream was provided and unable to get data from previous message`);
                }
                else {
                    dataBytes = Encoder.base64UrlToBytes(newestWithData.encodedData);
                }
            }
            else {
                dataBytes = yield DataStream.toBytes(dataStream);
            }
            const dataCid = yield Cid.computeDagPbCidFromBytes(dataBytes);
            RecordsWriteHandler.validateDataIntegrity(message.descriptor.dataCid, message.descriptor.dataSize, dataCid, dataBytes.length);
            const recordsWrite = Object.assign({}, message);
            recordsWrite.encodedData = Encoder.bytesToBase64Url(dataBytes);
            return recordsWrite;
        });
    }
    /**
     * Puts the given data in storage unless tenant already has that data for the given recordId
     *
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteMissingDataAssociation`
     *                    if `dataStream` is absent AND unable to associate data given `dataCid`
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataCidMismatch`
     *                    if the data stream resulted in a data CID that mismatches with `dataCid` in the given message
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataSizeMismatch`
     *                    if `dataSize` in `descriptor` given mismatches the actual data size
     */
    putData(tenant, message, dataStream) {
        return __awaiter(this, void 0, void 0, function* () {
            let result;
            const messageCid = yield Message.getCid(message);
            if (dataStream === undefined) {
                const associateResult = yield this.dataStore.associate(tenant, messageCid, message.descriptor.dataCid);
                if (associateResult === undefined) {
                    throw new DwnError(DwnErrorCode.RecordsWriteMissingDataAssociation, `Unable to associate dataCid ${message.descriptor.dataCid} ` +
                        `to messageCid ${messageCid} because dataStream was not provided and data was not found in dataStore`);
                }
                result = associateResult;
            }
            else {
                result = yield this.dataStore.put(tenant, messageCid, message.descriptor.dataCid, dataStream);
            }
            try {
                RecordsWriteHandler.validateDataIntegrity(message.descriptor.dataCid, message.descriptor.dataSize, result.dataCid, result.dataSize);
            }
            catch (error) {
                // delete data and throw error to caller
                yield this.dataStore.delete(tenant, messageCid, message.descriptor.dataCid);
                throw error;
            }
        });
    }
    /**
     * Validates the expected `dataCid` and `dataSize` in the descriptor vs the received data.
     *
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataCidMismatch`
     *                    if the data stream resulted in a data CID that mismatches with `dataCid` in the given message
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteDataSizeMismatch`
     *                    if `dataSize` in `descriptor` given mismatches the actual data size
     */
    static validateDataIntegrity(expectedDataCid, expectedDataSize, actualDataCid, actualDataSize) {
        if (expectedDataCid !== actualDataCid) {
            throw new DwnError(DwnErrorCode.RecordsWriteDataCidMismatch, `actual data CID ${actualDataCid} does not match dataCid in descriptor: ${expectedDataCid}`);
        }
        if (expectedDataSize !== actualDataSize) {
            throw new DwnError(DwnErrorCode.RecordsWriteDataSizeMismatch, `actual data size ${actualDataSize} bytes does not match dataSize in descriptor: ${expectedDataSize}`);
        }
    }
}
export function constructRecordsWriteIndexes(recordsWrite, isLatestBaseState) {
    return __awaiter(this, void 0, void 0, function* () {
        const message = recordsWrite.message;
        const descriptor = Object.assign({}, message.descriptor);
        delete descriptor.published; // handle `published` specifically further down
        const indexes = Object.assign(Object.assign({}, descriptor), { isLatestBaseState, published: !!message.descriptor.published, author: recordsWrite.author, recordId: message.recordId, entryId: yield RecordsWrite.getEntryId(recordsWrite.author, recordsWrite.message.descriptor) });
        // add additional indexes to optional values if given
        if (recordsWrite.attesters.length > 0) {
            indexes.attester = recordsWrite.attesters[0];
        }
        if (message.contextId !== undefined) {
            indexes.contextId = message.contextId;
        }
        return indexes;
    });
}
//# sourceMappingURL=records-write.js.map