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());
    });
};
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
import { classifySyncMessageScope } from './sync-scope-acceptance.js';
import { DwnInterface } from './types/dwn.js';
import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
import { orderMessagesForAdmission } from './sync-admission-order.js';
import { capRecordsWriteDataStream, fetchRemoteMessages, getMessageCid, SyncDataSizeLimitExceededError, SyncPullAbortedError, } from './sync-messages.js';
import { dependencyKey, hasTerminalDependency, isTenantProtocolConfig, missingDependencyDetail, newestProtocolConfig, resolveDelegatePermissionGrantId, } from './sync-fetch-helpers.js';
import { DwnInterfaceName, DwnMethodName, Encoder, Message, RecordsWrite } from '@enbox/dwn-sdk-js';
// applyReplicatedMessage reports the full set of missing ancestors in a single
// Incomplete, so a well-formed closure converges in a handful of passes. Keep a
// high cap anyway as a safety bound against malformed or adversarial remotes (and
// dependencies that only become fetchable across passes).
const MAX_ADMISSION_PASSES = 128;
/**
 * Applies a sync root and any dependencies it discovers through the DWN's
 * structured replication result. The DWN decides whether a message is applied,
 * duplicate, superseded, incomplete, invalid, or deferred; this module only
 * fetches missing dependencies and retries in dependency order.
 */
export function admitClosure(rootCid, deps) {
    return __awaiter(this, void 0, void 0, function* () {
        const context = new AdmitClosureContext(deps);
        return context.admit(rootCid);
    });
}
class AdmitClosureContext {
    constructor(deps) {
        var _a;
        this.deps = deps;
        this.entriesByCid = new Map();
        this.fetchedDependencyEntries = new Map();
        this.fetchedRefs = new Set();
        this.prefetchedEntries = (_a = deps.prefetched) !== null && _a !== void 0 ? _a : [];
    }
    admit(rootCid) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.rememberEntries(this.prefetchedEntries);
            let pending = yield this.initialPending(rootCid);
            const appliedCids = [];
            if (pending.length === 0) {
                return { kind: 'deferred', rootCid, detail: 'root message not available' };
            }
            for (let pass = 0; pass < MAX_ADMISSION_PASSES && pending.length > 0; pass++) {
                this.assertShouldContinue();
                const passResult = yield this.admitPass(rootCid, pending);
                if (passResult.kind === 'done') {
                    return passResult.outcome;
                }
                appliedCids.push(...passResult.appliedCids);
                pending = yield dedupeEntries(passResult.retry);
            }
            return pending.length === 0
                ? { kind: 'admitted', appliedCids }
                : { kind: 'deferred', rootCid, detail: 'dependency admission pass budget exhausted' };
        });
    }
    admitPass(rootCid, pending) {
        return __awaiter(this, void 0, void 0, function* () {
            const retry = [];
            const appliedCids = [];
            for (const entry of orderMessagesForAdmission(pending)) {
                this.assertShouldContinue();
                const result = yield this.admitEntry(rootCid, entry);
                switch (result.kind) {
                    case 'applied':
                        appliedCids.push(result.cid);
                        break;
                    case 'retry':
                        retry.push(...result.entries);
                        break;
                    case 'done':
                        return { kind: 'done', outcome: result.outcome };
                }
            }
            return { kind: 'continue', appliedCids, retry };
        });
    }
    admitEntry(rootCid, entry) {
        return __awaiter(this, void 0, void 0, function* () {
            const cid = yield this.rememberEntry(entry);
            if (cid === rootCid && !(yield this.rootIsInScope(entry))) {
                return {
                    kind: 'done',
                    outcome: { kind: 'failed', rootCid, reason: 'terminal', detail: 'root message is outside the sync scope' },
                };
            }
            const dataStream = yield replayableDataStream(entry);
            if (entryRequiresDataBeforeApply(entry) && dataStream === undefined) {
                return {
                    kind: 'done',
                    outcome: { kind: 'failed', rootCid, reason: 'terminal', detail: 'latest records write data is unavailable' },
                };
            }
            try {
                return this.admissionResultFromApply(rootCid, entry, cid, yield this.applyEntry(entry, dataStream));
            }
            catch (error) {
                if (error instanceof SyncDataSizeLimitExceededError) {
                    return {
                        kind: 'done',
                        outcome: { kind: 'failed', rootCid, reason: 'invalid', detail: error.message },
                    };
                }
                throw error;
            }
        });
    }
    admissionResultFromApply(rootCid, entry, cid, result) {
        return __awaiter(this, void 0, void 0, function* () {
            switch (result.kind) {
                case 'Applied':
                case 'Duplicate':
                case 'Superseded':
                    return { kind: 'applied', cid };
                case 'Deferred':
                    return { kind: 'done', outcome: { kind: 'deferred', rootCid, detail: result.reason } };
                case 'Invalid':
                    return { kind: 'done', outcome: { kind: 'failed', rootCid, reason: 'invalid', detail: result.reason } };
                case 'Incomplete':
                    return this.admissionResultFromMissingDependencies(rootCid, entry, result.missing);
            }
        });
    }
    admissionResultFromMissingDependencies(rootCid, entry, missing) {
        return __awaiter(this, void 0, void 0, function* () {
            if (hasTerminalDependency(missing)) {
                return {
                    kind: 'done',
                    outcome: { kind: 'failed', rootCid, reason: 'terminal', detail: missingDependencyDetail(missing) },
                };
            }
            const dependencies = yield this.fetchMissingDependencies(missing);
            if (dependencies.length === 0) {
                return { kind: 'done', outcome: { kind: 'deferred', rootCid, detail: missingDependencyDetail(missing) } };
            }
            return { kind: 'retry', entries: [...dependencies, entry] };
        });
    }
    initialPending(rootCid) {
        return __awaiter(this, void 0, void 0, function* () {
            const existing = this.entriesByCid.get(rootCid);
            if (existing !== undefined) {
                return [existing];
            }
            const fetched = yield fetchRemoteMessages({
                did: this.deps.did,
                dwnUrl: this.deps.dwnUrl,
                delegateDid: this.deps.delegateDid,
                permissionGrantIds: this.deps.permissionGrantIds,
                messageCids: [rootCid],
                agent: this.deps.agent,
            });
            return fetched;
        });
    }
    rootIsInScope(entry) {
        return __awaiter(this, void 0, void 0, function* () {
            const { scope } = this.deps;
            if (scope === undefined || scope.kind === 'full') {
                return true;
            }
            const classification = classifySyncMessageScope({
                message: entry.message,
                initialWrite: yield this.getInitialWriteForDelete(entry.message),
                scope,
            });
            return classification === 'in-scope';
        });
    }
    getInitialWriteForDelete(message) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            if (message.descriptor.interface !== DwnInterfaceName.Records ||
                message.descriptor.method !== DwnMethodName.Delete) {
                return undefined;
            }
            const recordId = message.descriptor.recordId;
            if (typeof recordId !== 'string') {
                return undefined;
            }
            for (const entry of this.entriesByCid.values()) {
                if (isInitialWriteForRecord(entry.message, recordId)) {
                    return entry.message;
                }
            }
            if (this.deps.agent.dwn.isRemoteMode) {
                return undefined;
            }
            const messageStore = (_b = (_a = this.deps.agent.dwn.node) === null || _a === void 0 ? void 0 : _a.storage) === null || _b === void 0 ? void 0 : _b.messageStore;
            if (messageStore === undefined) {
                return undefined;
            }
            return RecordsWrite.fetchInitialRecordsWriteMessage(messageStore, this.deps.did, recordId);
        });
    }
    applyEntry(entry, dataStream) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.deps.agent.dwn.applyReplicatedMessage(this.deps.did, entry.message, {
                dataStream,
            });
        });
    }
    fetchMissingDependencies(refs) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const fetched = [];
            for (const ref of refs) {
                const key = dependencyKey(ref);
                if (this.fetchedRefs.has(key)) {
                    fetched.push(...((_a = this.fetchedDependencyEntries.get(key)) !== null && _a !== void 0 ? _a : []));
                    continue;
                }
                this.fetchedRefs.add(key);
                const entries = yield this.fetchDependency(ref);
                this.fetchedDependencyEntries.set(key, entries);
                fetched.push(...entries);
            }
            return fetched;
        });
    }
    fetchDependency(ref) {
        return __awaiter(this, void 0, void 0, function* () {
            if (ref.messageCid !== undefined) {
                return this.fetchMessageCids([ref.messageCid]);
            }
            switch (ref.type) {
                case 'Protocol':
                    return this.fetchProtocolConfig(ref.protocol);
                case 'Parent':
                case 'Ancestor':
                case 'InitialWrite':
                    return this.fetchInitialWriteDependency(ref);
                case 'CrossProtocolRef':
                    return this.fetchRecordsByRecordId(ref.recordId, ref.protocol);
                case 'Role':
                    return this.fetchRoleRecord(ref);
                case 'Grant':
                    return this.fetchGrantRecord(ref.permissionGrantId);
                case 'KeyDelivery':
                    return this.fetchKeyDeliveryRecord(ref);
                case 'RecordData':
                    return this.fetchRecordData(ref);
                default:
                    return [];
            }
        });
    }
    fetchMessageCids(messageCids) {
        return __awaiter(this, void 0, void 0, function* () {
            const entries = yield fetchRemoteMessages({
                did: this.deps.did,
                dwnUrl: this.deps.dwnUrl,
                delegateDid: this.deps.delegateDid,
                permissionGrantIds: this.deps.permissionGrantIds,
                messageCids,
                agent: this.deps.agent,
            });
            yield this.rememberEntries(entries);
            return entries;
        });
    }
    fetchInitialWriteDependency(ref) {
        return __awaiter(this, void 0, void 0, function* () {
            const existing = this.findInitialWriteEntry(ref.recordId);
            if (existing !== undefined) {
                return [existing];
            }
            return this.fetchRecordsByRecordId(ref.recordId, ref.protocol);
        });
    }
    fetchGrantRecord(permissionGrantId) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.fetchRecordsByRecordId(permissionGrantId);
        });
    }
    fetchRecordsByRecordId(recordId, protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const permissionGrantId = protocol === undefined
                ? undefined
                : yield resolveDelegatePermissionGrantId(this.deps, DwnInterface.RecordsQuery, protocol);
            const granteeDid = permissionGrantId === undefined ? undefined : this.deps.delegateDid;
            const { message } = yield this.deps.agent.dwn.processRequest({
                author: (_a = this.deps.delegateDid) !== null && _a !== void 0 ? _a : this.deps.did,
                granteeDid,
                messageParams: Object.assign({ filter: Object.assign({ recordId }, (protocol === undefined ? {} : { protocol })) }, (permissionGrantId === undefined ? {} : { permissionGrantId })),
                messageType: DwnInterface.RecordsQuery,
                store: false,
                target: this.deps.did,
            });
            const reply = yield this.deps.agent.rpc.sendDwnRequest({
                dwnUrl: this.deps.dwnUrl,
                message,
                targetDid: this.deps.did,
            });
            return this.entriesFromRecordsQueryReply(reply);
        });
    }
    fetchRoleRecord(ref) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const permissionGrantId = yield resolveDelegatePermissionGrantId(this.deps, DwnInterface.RecordsQuery, ref.protocol);
            const granteeDid = permissionGrantId === undefined ? undefined : this.deps.delegateDid;
            const { message } = yield this.deps.agent.dwn.processRequest({
                author: (_a = this.deps.delegateDid) !== null && _a !== void 0 ? _a : this.deps.did,
                granteeDid,
                messageParams: Object.assign({ filter: Object.assign({ protocol: ref.protocol, protocolPath: ref.protocolPath, recipient: ref.recipient }, (ref.contextPrefix === undefined ? {} : { contextId: ref.contextPrefix })) }, (permissionGrantId === undefined ? {} : { permissionGrantId })),
                messageType: DwnInterface.RecordsQuery,
                store: false,
                target: this.deps.did,
            });
            const reply = yield this.deps.agent.rpc.sendDwnRequest({
                dwnUrl: this.deps.dwnUrl,
                message,
                targetDid: this.deps.did,
            });
            return this.entriesFromRecordsQueryReply(reply);
        });
    }
    fetchKeyDeliveryRecord(ref) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const { message } = yield this.deps.agent.dwn.processRequest({
                author: (_a = this.deps.delegateDid) !== null && _a !== void 0 ? _a : this.deps.did,
                messageParams: {
                    filter: {
                        protocol: KeyDeliveryProtocolDefinition.protocol,
                        protocolPath: 'contextKey',
                        tags: {
                            protocol: ref.protocol,
                            contextId: ref.contextId,
                        },
                    },
                },
                messageType: DwnInterface.RecordsQuery,
                store: false,
                target: this.deps.did,
            });
            const reply = yield this.deps.agent.rpc.sendDwnRequest({
                dwnUrl: this.deps.dwnUrl,
                message,
                targetDid: this.deps.did,
            });
            return this.entriesFromRecordsQueryReply(reply);
        });
    }
    entriesFromRecordsQueryReply(reply) {
        return __awaiter(this, void 0, void 0, function* () {
            if (reply.status.code !== 200 || reply.entries === undefined) {
                return [];
            }
            const entries = reply.entries.flatMap((entry) => {
                const { encodedData, initialWrite } = entry, message = __rest(entry, ["encodedData", "initialWrite"]);
                const syncEntries = [];
                if (initialWrite !== undefined) {
                    syncEntries.push({ message: initialWrite });
                }
                const syncEntry = { message };
                if (encodedData !== undefined) {
                    syncEntry.bufferedData = Encoder.base64UrlToBytes(encodedData);
                }
                syncEntries.push(syncEntry);
                return syncEntries;
            });
            const dedupedEntries = yield dedupeEntries(entries);
            yield this.rememberEntries(dedupedEntries);
            return dedupedEntries;
        });
    }
    fetchRecordData(ref) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            const permissionGrantId = ref.protocol === undefined
                ? undefined
                : yield resolveDelegatePermissionGrantId(this.deps, DwnInterface.RecordsRead, ref.protocol);
            const granteeDid = permissionGrantId === undefined ? undefined : this.deps.delegateDid;
            const { message } = yield this.deps.agent.dwn.processRequest({
                author: (_a = this.deps.delegateDid) !== null && _a !== void 0 ? _a : this.deps.did,
                granteeDid,
                messageParams: Object.assign({ filter: { recordId: ref.recordId } }, (permissionGrantId === undefined ? {} : { permissionGrantId })),
                messageType: DwnInterface.RecordsRead,
                store: false,
                target: this.deps.did,
            });
            const reply = yield this.deps.agent.rpc.sendDwnRequest({
                dwnUrl: this.deps.dwnUrl,
                message,
                targetDid: this.deps.did,
            });
            if (reply.status.code !== 200 || ((_b = reply.entry) === null || _b === void 0 ? void 0 : _b.recordsWrite) === undefined || reply.entry.data === undefined) {
                return [];
            }
            const recordsWrite = reply.entry.recordsWrite;
            if (recordsWrite.descriptor.dataCid !== ref.dataCid) {
                return [];
            }
            const entry = { message: recordsWrite, dataStream: capRecordsWriteDataStream(recordsWrite, reply.entry.data) };
            yield this.rememberEntry(entry);
            return [entry];
        });
    }
    fetchProtocolConfig(protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            const config = yield this.fetchProtocol(protocol);
            const entries = config === undefined ? [] : [{ message: config }];
            yield this.rememberEntries(entries);
            return entries;
        });
    }
    fetchProtocol(protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const permissionGrantId = yield resolveDelegatePermissionGrantId(this.deps, DwnInterface.ProtocolsQuery, protocol);
            const granteeDid = permissionGrantId === undefined ? undefined : this.deps.delegateDid;
            const { message } = yield this.deps.agent.dwn.processRequest({
                author: (_a = this.deps.delegateDid) !== null && _a !== void 0 ? _a : this.deps.did,
                granteeDid,
                messageParams: Object.assign({ filter: { protocol } }, (permissionGrantId === undefined ? {} : { permissionGrantId })),
                messageType: DwnInterface.ProtocolsQuery,
                store: false,
                target: this.deps.did,
            });
            const reply = yield this.deps.agent.rpc.sendDwnRequest({
                dwnUrl: this.deps.dwnUrl,
                message,
                targetDid: this.deps.did,
            });
            if (reply.status.code !== 200 || reply.entries === undefined) {
                return undefined;
            }
            const configs = reply.entries.filter(isTenantProtocolConfig(this.deps.did, protocol));
            return newestProtocolConfig(configs);
        });
    }
    rememberEntries(entries) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const entry of entries) {
                yield this.rememberEntry(entry);
            }
        });
    }
    rememberEntry(entry) {
        return __awaiter(this, void 0, void 0, function* () {
            const cid = yield Message.getCid(entry.message);
            this.entriesByCid.set(cid, entry);
            return cid;
        });
    }
    assertShouldContinue() {
        var _a, _b;
        if (((_b = (_a = this.deps).shouldContinue) === null || _b === void 0 ? void 0 : _b.call(_a)) === false) {
            throw new SyncPullAbortedError();
        }
    }
    findInitialWriteEntry(recordId) {
        for (const entry of this.entriesByCid.values()) {
            if (isInitialWriteForRecord(entry.message, recordId)) {
                return entry;
            }
        }
    }
}
function replayableDataStream(entry) {
    return __awaiter(this, void 0, void 0, function* () {
        if (entry.bufferedData === undefined) {
            return entry.dataStreamFactory === undefined ? entry.dataStream : entry.dataStreamFactory();
        }
        return new ReadableStream({
            start(controller) {
                controller.enqueue(entry.bufferedData);
                controller.close();
            }
        });
    });
}
function entryRequiresDataBeforeApply(entry) {
    return entry.isLatestBaseState === true && recordsWriteRequiresData(entry.message);
}
function recordsWriteRequiresData(message) {
    if (message.descriptor.interface !== DwnInterfaceName.Records ||
        message.descriptor.method !== DwnMethodName.Write) {
        return false;
    }
    return typeof message.descriptor.dataCid === 'string';
}
function dedupeEntries(entries) {
    return __awaiter(this, void 0, void 0, function* () {
        const byCid = new Map();
        for (const entry of entries) {
            byCid.set(yield getMessageCid(entry.message), entry);
        }
        return [...byCid.values()];
    });
}
function isInitialWriteForRecord(message, recordId) {
    if (message.descriptor.interface !== DwnInterfaceName.Records ||
        message.descriptor.method !== DwnMethodName.Write) {
        return false;
    }
    const recordsWrite = message;
    return recordsWrite.recordId === recordId &&
        recordsWrite.descriptor.dateCreated === recordsWrite.descriptor.messageTimestamp;
}
//# sourceMappingURL=sync-admit-closure.js.map