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 { DwnInterfaceName, DwnMethodName, Message } from '@enbox/dwn-sdk-js';
import { DwnInterface } from './types/dwn.js';
import { isRecordsWrite } from './utils.js';
import { topologicalSort } from './sync-topological-sort.js';
/**
 * 202: message was successfully written to the remote DWN
 * 204: an initial write message was written without any data
 * 409: message was already present on the remote DWN
 * RecordsDelete + 404: the initial write was not found or already deleted
 */
export function syncMessageReplyIsSuccessful(reply) {
    var _a, _b;
    return reply.status.code === 202 ||
        reply.status.code === 204 ||
        reply.status.code === 409 ||
        (((_a = reply.entry) === null || _a === void 0 ? void 0 : _a.message.descriptor.interface) === DwnInterfaceName.Records &&
            ((_b = reply.entry) === null || _b === void 0 ? void 0 : _b.message.descriptor.method) === DwnMethodName.Delete &&
            reply.status.code === 404);
}
/**
 * Helper to get the CID of a message for logging purposes.
 */
export function getMessageCid(message) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            return yield Message.getCid(message);
        }
        catch (_a) {
            return 'unknown';
        }
    });
}
/**
 * Fetches missing messages from the remote DWN and processes them on the local DWN
 * in dependency order (topological sort).
 *
 * Messages that fail processing are re-fetched from the remote before each retry
 * pass rather than buffered in memory. ReadableStream is single-use, so a failed
 * message's data stream is consumed on the first attempt. Re-fetching provides a
 * fresh stream without holding all record data in memory simultaneously.
 */
export function pullMessages(_a) {
    return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi }) {
        // Step 1: Fetch all missing messages from the remote in parallel.
        const fetched = yield fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi });
        // Step 2: Build dependency graph and topological sort.
        const sorted = topologicalSort(fetched);
        // Step 3: Process messages in dependency order with multi-pass retry.
        // Retry up to MAX_RETRY_PASSES times for messages that fail due to
        // dependency ordering issues (e.g., a RecordsWrite whose ProtocolsConfigure
        // hasn't committed yet). Failed messages are re-fetched from the remote
        // to obtain a fresh data stream, since ReadableStream is single-use.
        const MAX_RETRY_PASSES = 3;
        let pending = sorted;
        for (let pass = 0; pass <= MAX_RETRY_PASSES && pending.length > 0; pass++) {
            const failedCids = [];
            for (const entry of pending) {
                const pullReply = yield agent.dwn.node.processMessage(did, entry.message, { dataStream: entry.dataStream });
                if (!syncMessageReplyIsSuccessful(pullReply)) {
                    const cid = yield getMessageCid(entry.message);
                    failedCids.push(cid);
                }
            }
            // Re-fetch failed messages from the remote to get fresh data streams.
            if (failedCids.length > 0) {
                const reFetched = yield fetchRemoteMessages({ did, dwnUrl, delegateDid, protocol, messageCids: failedCids, agent, permissionsApi });
                pending = topologicalSort(reFetched);
            }
            else {
                pending = [];
            }
        }
    });
}
/**
 * Fetches messages from a remote DWN by their CIDs using MessagesRead.
 */
export function fetchRemoteMessages(_a) {
    return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi }) {
        const results = [];
        let permissionGrantId;
        if (delegateDid) {
            try {
                const messagesReadGrant = yield permissionsApi.getPermissionForRequest({
                    connectedDid: did,
                    messageType: DwnInterface.MessagesRead,
                    delegateDid,
                    protocol,
                    cached: true
                });
                permissionGrantId = messagesReadGrant.grant.id;
            }
            catch (error) {
                console.error('SyncEngineLevel: pull - Error fetching MessagesRead permission grant for delegate DID', error);
                return results;
            }
        }
        // Fetch messages in parallel with bounded concurrency.
        const CONCURRENCY = 10;
        let cursor = 0;
        while (cursor < messageCids.length) {
            const batch = messageCids.slice(cursor, cursor + CONCURRENCY);
            cursor += CONCURRENCY;
            const batchResults = yield Promise.all(batch.map((messageCid) => __awaiter(this, void 0, void 0, function* () {
                var _a, _b;
                const messagesRead = yield agent.processDwnRequest({
                    store: false,
                    author: did,
                    target: did,
                    messageType: DwnInterface.MessagesRead,
                    granteeDid: delegateDid,
                    messageParams: { messageCid, permissionGrantId }
                });
                let reply;
                try {
                    reply = (yield agent.rpc.sendDwnRequest({
                        dwnUrl,
                        targetDid: did,
                        message: messagesRead.message,
                    }));
                }
                catch (error) {
                    console.error(`SyncEngineLevel: pull - failed to read ${messageCid} from ${dwnUrl}:`, (_a = error.message) !== null && _a !== void 0 ? _a : error);
                    return undefined;
                }
                if (reply.status.code !== 200 || !((_b = reply.entry) === null || _b === void 0 ? void 0 : _b.message)) {
                    return undefined;
                }
                const replyEntry = reply.entry;
                let dataStream;
                if (isRecordsWrite(replyEntry) && replyEntry.data) {
                    dataStream = replyEntry.data;
                }
                return { message: replyEntry.message, dataStream };
            })));
            for (const result of batchResults) {
                if (result) {
                    results.push(result);
                }
            }
        }
        return results;
    });
}
/**
 * Reads missing messages from the local DWN and pushes them to the remote DWN.
 * Messages are fetched first, then sorted in dependency order (topological sort)
 * so that initial writes come before updates, and ProtocolsConfigures come before
 * records that reference those protocols.
 */
export function pushMessages(_a) {
    return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids, agent, permissionsApi }) {
        var _b;
        // Step 1: Fetch all local messages (streams are pull-based, not yet consumed).
        const fetched = [];
        for (const messageCid of messageCids) {
            const dwnMessage = yield getLocalMessage({ author: did, messageCid, delegateDid, protocol, agent, permissionsApi });
            if (dwnMessage) {
                fetched.push(dwnMessage);
            }
        }
        // Step 2: Sort in dependency order using topological sort.
        const sorted = topologicalSort(fetched);
        // Step 3: Push messages in dependency order, consuming each stream as we go.
        for (const entry of sorted) {
            try {
                const reply = yield agent.rpc.sendDwnRequest({
                    dwnUrl,
                    targetDid: did,
                    data: entry.dataStream,
                    message: entry.message
                });
                if (!syncMessageReplyIsSuccessful(reply)) {
                    const cid = yield getMessageCid(entry.message);
                    console.error(`SyncEngineLevel: push failed for ${cid}: ${reply.status.code} ${reply.status.detail}`);
                }
            }
            catch (error) {
                // Preserve the original error so callers can diagnose the root cause.
                const detail = (_b = error.message) !== null && _b !== void 0 ? _b : error;
                throw new Error(`SyncEngineLevel: push to ${dwnUrl} failed: ${detail}`);
            }
        }
    });
}
/**
 * Reads a message from the local DWN by its CID using MessagesRead.
 */
export function getLocalMessage(_a) {
    return __awaiter(this, arguments, void 0, function* ({ author, delegateDid, protocol, messageCid, agent, permissionsApi }) {
        let permissionGrantId;
        if (delegateDid) {
            try {
                const messagesReadGrant = yield permissionsApi.getPermissionForRequest({
                    connectedDid: author,
                    messageType: DwnInterface.MessagesRead,
                    delegateDid,
                    protocol,
                    cached: true
                });
                permissionGrantId = messagesReadGrant.grant.id;
            }
            catch (error) {
                console.error('SyncEngineLevel: push - Error fetching MessagesRead permission grant for delegate DID', error);
                return;
            }
        }
        const { reply } = yield agent.dwn.processRequest({
            author,
            target: author,
            messageType: DwnInterface.MessagesRead,
            granteeDid: delegateDid,
            messageParams: { messageCid, permissionGrantId }
        });
        if (reply.status.code !== 200 || !reply.entry) {
            return undefined;
        }
        const messageEntry = reply.entry;
        const result = {
            message: messageEntry.message
        };
        if (isRecordsWrite(messageEntry) && messageEntry.data) {
            result.dataStream = messageEntry.data;
        }
        return result;
    });
}
//# sourceMappingURL=sync-messages.js.map