"use strict";
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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.constructRecordsWriteIndexes = exports.RecordsWriteHandler = void 0;
const auth_js_1 = require("../core/auth.js");
const message_reply_js_1 = require("../core/message-reply.js");
const records_write_js_1 = require("../interfaces/records-write.js");
const storage_controller_js_1 = require("../store/storage-controller.js");
const dwn_error_js_1 = require("../core/dwn-error.js");
const message_js_1 = require("../core/message.js");
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 records_write_js_1.RecordsWrite.parse(message);
            }
            catch (e) {
                return (0, message_reply_js_1.messageReplyFromError)(e, 400);
            }
            // authentication & authorization
            try {
                yield (0, auth_js_1.authenticate)(message.authorization, this.didResolver);
                yield recordsWrite.authorize(tenant, this.messageStore);
            }
            catch (e) {
                return (0, message_reply_js_1.messageReplyFromError)(e, 401);
            }
            // get existing messages matching the `recordId`
            const query = {
                interface: message_js_1.DwnInterfaceName.Records,
                recordId: message.recordId
            };
            const 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 records_write_js_1.RecordsWrite.getInitialWrite(existingMessages);
                    records_write_js_1.RecordsWrite.verifyEqualityOfImmutableProperties(initialWrite, message);
                }
                catch (e) {
                    return (0, message_reply_js_1.messageReplyFromError)(e, 400);
                }
            }
            const newestExistingMessage = yield message_js_1.Message.getNewestMessage(existingMessages);
            let incomingMessageIsNewest = false;
            let newestMessage; // keep reference of newest message for pruning later
            if (newestExistingMessage === undefined || (yield message_js_1.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);
            try {
                // try to store data, unless options explicitly say to skip storage
                if (options === undefined || !options.skipDataStorage) {
                    yield this.putData(tenant, message, dataStream, newestExistingMessage);
                }
            }
            catch (error) {
                const e = error;
                if (e.code === dwn_error_js_1.DwnErrorCode.RecordsWriteMissingDataStream ||
                    e.code === dwn_error_js_1.DwnErrorCode.RecordsWriteMissingData ||
                    e.code === dwn_error_js_1.DwnErrorCode.RecordsWriteDataCidMismatch ||
                    e.code === dwn_error_js_1.DwnErrorCode.RecordsWriteDataSizeMismatch) {
                    return (0, message_reply_js_1.messageReplyFromError)(error, 400);
                }
                // else throw
                throw error;
            }
            yield this.messageStore.put(tenant, message, indexes);
            yield this.eventLog.append(tenant, yield message_js_1.Message.getCid(message));
            const messageReply = {
                status: { code: 202, detail: 'Accepted' }
            };
            // delete all existing messages that are not newest, except for the initial write
            yield storage_controller_js_1.StorageController.deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore, this.dataStore, this.eventLog);
            return messageReply;
        });
    }
    ;
    /**
     * Puts the given data in storage unless tenant already has that data for the given recordId
     *
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteMissingDataStream`
     *                    if `dataStream` is absent AND the `dataCid` does not match the current data for the given recordId
     * @throws {DwnError} with `DwnErrorCode.RecordsWriteMissingData`
     *                    if `dataStream` is absent AND dataStore does not contain the 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, newestExistingMessage) {
        return __awaiter(this, void 0, void 0, function* () {
            let result;
            const messageCid = yield message_js_1.Message.getCid(message);
            if (dataStream === undefined) {
                // dataStream must be included if message contains a new dataCid
                if ((newestExistingMessage === null || newestExistingMessage === void 0 ? void 0 : newestExistingMessage.descriptor.method) === message_js_1.DwnMethodName.Delete ||
                    (newestExistingMessage === null || newestExistingMessage === void 0 ? void 0 : newestExistingMessage.descriptor.dataCid) !== message.descriptor.dataCid) {
                    throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.RecordsWriteMissingDataStream, 'Data stream is not provided.');
                }
                const associateResult = yield this.dataStore.associate(tenant, messageCid, message.descriptor.dataCid);
                if (associateResult === undefined) {
                    throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.RecordsWriteMissingData, `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);
            }
            // verify that given dataSize matches size of actual data
            if (message.descriptor.dataSize !== result.dataSize) {
                // there is an opportunity to improve here: handle the edge case of if the delete fails...
                yield this.dataStore.delete(tenant, messageCid, message.descriptor.dataCid);
                throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.RecordsWriteDataSizeMismatch, `actual data size ${result.dataSize} bytes does not match dataSize in descriptor: ${message.descriptor.dataSize}`);
            }
            // verify that given dataCid matches CID of actual data
            if (message.descriptor.dataCid !== result.dataCid) {
                // there is an opportunity to improve here: handle the edge cae of if the delete fails...
                yield this.dataStore.delete(tenant, messageCid, message.descriptor.dataCid);
                throw new dwn_error_js_1.DwnError(dwn_error_js_1.DwnErrorCode.RecordsWriteDataCidMismatch, `actual data CID ${result.dataCid} does not match dataCid in descriptor: ${message.descriptor.dataCid}`);
            }
        });
    }
}
exports.RecordsWriteHandler = RecordsWriteHandler;
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 records_write_js_1.RecordsWrite.getEntryId(recordsWrite.author, recordsWrite.message.descriptor) });
        if (recordsWrite.attesters.length > 0) {
            indexes.attester = recordsWrite.attesters[0];
        }
        if (message.contextId !== undefined) {
            indexes.contextId = message.contextId;
        }
        return indexes;
    });
}
exports.constructRecordsWriteIndexes = constructRecordsWriteIndexes;
//# sourceMappingURL=records-write.js.map