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 __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
import ms from 'ms';
import { Level } from 'level';
import { hashToHex, initDefaultHashes, Message } from '@enbox/dwn-sdk-js';
import { AgentPermissionsApi } from './permissions-api.js';
import { DwnInterface } from './types/dwn.js';
import { getDwnServiceEndpointUrls } from './utils.js';
import { isRecordsWrite } from './utils.js';
import { topologicalSort } from './sync-topological-sort.js';
import { pullMessages, pushMessages } from './sync-messages.js';
/**
 * Maximum bit prefix depth before falling back to leaf enumeration.
 * At depth 16, each subtree covers ~1/65536 of the key space, which is a good
 * balance between round-trip count and leaf-set size.
 */
const MAX_DIFF_DEPTH = 16;
/**
 * Key for the subscription cursor sublevel. Cursors are keyed by
 * `{did}^{dwnUrl}[^{protocol}]` and store an opaque EventLog cursor string.
 */
const CURSOR_SEPARATOR = '^';
/**
 * Debounce window for push-on-write. When the local EventLog emits events,
 * we batch them and push after this delay to avoid a push per individual write.
 */
const PUSH_DEBOUNCE_MS = 250;
export class SyncEngineLevel {
    constructor({ agent, dataPath, db }) {
        this._syncLock = false;
        // ---------------------------------------------------------------------------
        // Live sync state
        // ---------------------------------------------------------------------------
        /** Current sync mode, set by `startSync`. */
        this._syncMode = 'poll';
        /** Active live pull subscriptions (remote -> local via MessagesSubscribe). */
        this._liveSubscriptions = [];
        /** Active local EventLog subscriptions for push-on-write (local -> remote). */
        this._localSubscriptions = [];
        /** Connectivity state derived from subscription health. */
        this._connectivityState = 'unknown';
        /** Pending message CIDs to push, accumulated during the debounce window. */
        this._pendingPushCids = new Map();
        /** Count of consecutive SMT sync failures (for backoff in poll mode). */
        this._consecutiveFailures = 0;
        this._agent = agent;
        this._permissionsApi = new AgentPermissionsApi({ agent: agent });
        this._db = (db) ? db : new Level(dataPath !== null && dataPath !== void 0 ? dataPath : 'DATA/AGENT/SYNC_STORE');
    }
    /**
     * Retrieves the `Web5PlatformAgent` execution context.
     *
     * @returns The `Web5PlatformAgent` instance that represents the current execution context.
     * @throws Will throw an error if the `agent` instance property is undefined.
     */
    get agent() {
        if (this._agent === undefined) {
            throw new Error('SyncEngineLevel: Unable to determine agent execution context.');
        }
        return this._agent;
    }
    set agent(agent) {
        this._agent = agent;
        this._permissionsApi = new AgentPermissionsApi({ agent: agent });
    }
    get connectivityState() {
        return this._connectivityState;
    }
    clear() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._permissionsApi.clear();
            yield this._db.clear();
        });
    }
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            yield this._db.close();
        });
    }
    registerIdentity(_a) {
        return __awaiter(this, arguments, void 0, function* ({ did, options }) {
            const registeredIdentities = this._db.sublevel('registeredIdentities');
            const existing = yield this.getIdentityOptions(did);
            if (existing) {
                throw new Error(`SyncEngineLevel: Identity with DID ${did} is already registered.`);
            }
            // if no options are provided, we default to no delegateDid and all protocols (empty array)
            options !== null && options !== void 0 ? options : (options = { protocols: [] });
            yield registeredIdentities.put(did, JSON.stringify(options));
        });
    }
    unregisterIdentity(did) {
        return __awaiter(this, void 0, void 0, function* () {
            const registeredIdentities = this._db.sublevel('registeredIdentities');
            const existing = yield this.getIdentityOptions(did);
            if (!existing) {
                throw new Error(`SyncEngineLevel: Identity with DID ${did} is not registered.`);
            }
            yield registeredIdentities.del(did);
        });
    }
    getIdentityOptions(did) {
        return __awaiter(this, void 0, void 0, function* () {
            const registeredIdentities = this._db.sublevel('registeredIdentities');
            try {
                const options = yield registeredIdentities.get(did);
                if (options) {
                    return JSON.parse(options);
                }
            }
            catch (error) {
                const e = error;
                // `Level` throws an error if the key is not present. Return `undefined` in this case.
                if (e.code === 'LEVEL_NOT_FOUND') {
                    return;
                }
                else {
                    throw new Error(`SyncEngineLevel: Error reading level: ${e.code}.`);
                }
            }
        });
    }
    updateIdentityOptions(_a) {
        return __awaiter(this, arguments, void 0, function* ({ did, options }) {
            const registeredIdentities = this._db.sublevel('registeredIdentities');
            const existingOptions = yield this.getIdentityOptions(did);
            if (!existingOptions) {
                throw new Error(`SyncEngineLevel: Identity with DID ${did} is not registered.`);
            }
            yield registeredIdentities.put(did, JSON.stringify(options));
        });
    }
    // ---------------------------------------------------------------------------
    // One-shot sync (SMT set reconciliation)
    // ---------------------------------------------------------------------------
    sync(direction) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this._syncLock) {
                throw new Error('SyncEngineLevel: Sync operation is already in progress.');
            }
            this._syncLock = true;
            try {
                // Iterate over all registered identities and their DWN endpoints.
                const syncTargets = yield this.getSyncTargets();
                const errored = new Set();
                let hadFailure = false;
                for (const target of syncTargets) {
                    const { did, delegateDid, dwnUrl, protocol } = target;
                    if (errored.has(dwnUrl)) {
                        continue;
                    }
                    try {
                        // Phase 1: Compare SMT roots between local and remote.
                        const localRoot = yield this.getLocalRoot(did, delegateDid, protocol);
                        const remoteRoot = yield this.getRemoteRoot(did, dwnUrl, delegateDid, protocol);
                        if (localRoot === remoteRoot) {
                            // Trees are identical — nothing to sync for this target.
                            continue;
                        }
                        // Phase 2: Walk the tree to find differing subtrees.
                        const diff = yield this.walkTreeDiff({
                            did, dwnUrl, delegateDid, protocol,
                        });
                        // Phase 3: Pull missing messages (remote has, local doesn't).
                        if (!direction || direction === 'pull') {
                            if (diff.onlyRemote.length > 0) {
                                yield this.pullMessages({ did, dwnUrl, delegateDid, protocol, messageCids: diff.onlyRemote });
                            }
                        }
                        // Phase 4: Push missing messages (local has, remote doesn't).
                        if (!direction || direction === 'push') {
                            if (diff.onlyLocal.length > 0) {
                                yield this.pushMessages({ did, dwnUrl, delegateDid, protocol, messageCids: diff.onlyLocal });
                            }
                        }
                    }
                    catch (error) {
                        // Skip this DWN endpoint for remaining targets and log the real cause.
                        errored.add(dwnUrl);
                        hadFailure = true;
                        console.error(`SyncEngineLevel: Error syncing ${did} with ${dwnUrl}`, error);
                    }
                }
                // Track consecutive failures for backoff in poll mode.
                if (hadFailure) {
                    this._consecutiveFailures++;
                    if (this._connectivityState === 'online') {
                        this._connectivityState = 'offline';
                    }
                }
                else {
                    this._consecutiveFailures = 0;
                    if (syncTargets.length > 0) {
                        this._connectivityState = 'online';
                    }
                }
            }
            finally {
                this._syncLock = false;
            }
        });
    }
    // ---------------------------------------------------------------------------
    // startSync / stopSync
    // ---------------------------------------------------------------------------
    startSync(params) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            const mode = (_a = params.mode) !== null && _a !== void 0 ? _a : 'poll';
            const intervalStr = (_b = params.interval) !== null && _b !== void 0 ? _b : (mode === 'live' ? '5m' : '2m');
            const intervalMilliseconds = ms(intervalStr);
            // Tear down previous mode if there are active live resources.
            if (this._liveSubscriptions.length > 0 || this._localSubscriptions.length > 0) {
                yield this.teardownLiveSync();
            }
            if (this._syncIntervalId) {
                clearInterval(this._syncIntervalId);
                this._syncIntervalId = undefined;
            }
            this._syncMode = mode;
            if (mode === 'live') {
                yield this.startLiveSync(intervalMilliseconds);
            }
            else {
                yield this.startPollSync(intervalMilliseconds);
            }
        });
    }
    /**
     * stopSync awaits the completion of the current sync operation before stopping the sync interval
     * and tearing down any live subscriptions.
     */
    stopSync() {
        return __awaiter(this, arguments, void 0, function* (timeout = 2000) {
            let elapsedTimeout = 0;
            while (this._syncLock) {
                if (elapsedTimeout >= timeout) {
                    throw new Error(`SyncEngineLevel: Existing sync operation did not complete within ${timeout} milliseconds.`);
                }
                elapsedTimeout += 100;
                yield new Promise((resolve) => { setTimeout(resolve, timeout < 100 ? timeout : 100); });
            }
            if (this._syncIntervalId) {
                clearInterval(this._syncIntervalId);
                this._syncIntervalId = undefined;
            }
            yield this.teardownLiveSync();
        });
    }
    // ---------------------------------------------------------------------------
    // Poll-mode sync (legacy)
    // ---------------------------------------------------------------------------
    startPollSync(intervalMilliseconds) {
        return __awaiter(this, void 0, void 0, function* () {
            const intervalSync = () => __awaiter(this, void 0, void 0, function* () {
                if (this._syncLock) {
                    return;
                }
                clearInterval(this._syncIntervalId);
                this._syncIntervalId = undefined;
                try {
                    yield this.sync();
                }
                catch (error) {
                    console.error('SyncEngineLevel: Error during sync operation', error);
                }
                // Apply backoff on consecutive failures.
                const backoffMultiplier = Math.min(Math.pow(2, this._consecutiveFailures), SyncEngineLevel.MAX_BACKOFF_MULTIPLIER);
                const effectiveInterval = this._consecutiveFailures > 0
                    ? intervalMilliseconds * backoffMultiplier
                    : intervalMilliseconds;
                if (!this._syncIntervalId) {
                    this._syncIntervalId = setInterval(intervalSync, effectiveInterval);
                }
            });
            if (this._syncIntervalId) {
                clearInterval(this._syncIntervalId);
            }
            this._syncIntervalId = setInterval(intervalSync, intervalMilliseconds);
            // Initiate an immediate sync.
            if (!this._syncLock) {
                yield this.sync();
            }
        });
    }
    // ---------------------------------------------------------------------------
    // Live-mode sync
    // ---------------------------------------------------------------------------
    /**
     * Starts live sync:
     * 1. Performs an initial SMT reconciliation to catch up.
     * 2. Opens MessagesSubscribe subscriptions to each remote DWN for real-time pull.
     * 3. Subscribes to the local EventLog for push-on-write.
     * 4. Schedules an infrequent SMT integrity check at `interval`.
     */
    startLiveSync(intervalMilliseconds) {
        return __awaiter(this, void 0, void 0, function* () {
            // Step 1: Initial SMT catch-up.
            try {
                yield this.sync();
            }
            catch (error) {
                console.error('SyncEngineLevel: Error during initial live-sync catch-up', error);
            }
            // Step 2: Open live subscriptions for each sync target.
            const syncTargets = yield this.getSyncTargets();
            for (const target of syncTargets) {
                try {
                    yield this.openLivePullSubscription(target);
                    yield this.openLocalPushSubscription(target);
                }
                catch (error) {
                    console.error(`SyncEngineLevel: Failed to open live subscription for ${target.did} -> ${target.dwnUrl}`, error);
                }
            }
            // Step 3: Schedule infrequent SMT integrity check.
            const integrityCheck = () => __awaiter(this, void 0, void 0, function* () {
                if (this._syncLock) {
                    return;
                }
                try {
                    yield this.sync();
                }
                catch (error) {
                    console.error('SyncEngineLevel: Error during SMT integrity check', error);
                }
            });
            this._syncIntervalId = setInterval(integrityCheck, intervalMilliseconds);
        });
    }
    /**
     * Tears down all live subscriptions and push listeners.
     */
    teardownLiveSync() {
        return __awaiter(this, void 0, void 0, function* () {
            // Clear the push debounce timer.
            if (this._pushDebounceTimer) {
                clearTimeout(this._pushDebounceTimer);
                this._pushDebounceTimer = undefined;
            }
            // Flush any pending push CIDs.
            this._pendingPushCids.clear();
            // Close all live pull subscriptions.
            for (const sub of this._liveSubscriptions) {
                try {
                    yield sub.close();
                }
                catch (_a) {
                    // Best-effort cleanup.
                }
            }
            this._liveSubscriptions = [];
            // Close all local push subscriptions.
            for (const sub of this._localSubscriptions) {
                try {
                    yield sub.close();
                }
                catch (_b) {
                    // Best-effort cleanup.
                }
            }
            this._localSubscriptions = [];
        });
    }
    // ---------------------------------------------------------------------------
    // Live pull: MessagesSubscribe to remote DWN
    // ---------------------------------------------------------------------------
    /**
     * Opens a MessagesSubscribe WebSocket subscription to a remote DWN.
     * Incoming events are processed locally as they arrive.
     */
    openLivePullSubscription(target) {
        return __awaiter(this, void 0, void 0, function* () {
            const { did, delegateDid, dwnUrl, protocol } = target;
            // Resolve the cursor from the last session (if any).
            const cursorKey = this.buildCursorKey(did, dwnUrl, protocol);
            const cursor = yield this.getCursor(cursorKey);
            // Build the MessagesSubscribe filters.
            const filters = protocol ? [{ protocol }] : [];
            // Look up permission grant for MessagesSubscribe if using a delegate.
            let permissionGrantId;
            if (delegateDid) {
                try {
                    const grant = yield this._permissionsApi.getPermissionForRequest({
                        connectedDid: did,
                        messageType: DwnInterface.MessagesSubscribe,
                        delegateDid,
                        protocol,
                        cached: true
                    });
                    permissionGrantId = grant.grant.id;
                }
                catch (_a) {
                    // Fall back to trying MessagesRead which is a unified scope.
                    try {
                        const grant = yield this._permissionsApi.getPermissionForRequest({
                            connectedDid: did,
                            messageType: DwnInterface.MessagesRead,
                            delegateDid,
                            protocol,
                            cached: true
                        });
                        permissionGrantId = grant.grant.id;
                    }
                    catch (error) {
                        console.error('SyncEngineLevel: Could not find permission grant for live pull subscription', error);
                        return;
                    }
                }
            }
            // Define the subscription handler that processes incoming events.
            const subscriptionHandler = (subMessage) => __awaiter(this, void 0, void 0, function* () {
                if (subMessage.type === 'eose') {
                    // End-of-stored-events — catch-up complete, persist cursor.
                    yield this.setCursor(cursorKey, subMessage.cursor);
                    this._connectivityState = 'online';
                    return;
                }
                if (subMessage.type === 'event') {
                    const event = subMessage.event;
                    try {
                        // Process the message locally.
                        const dataStream = this.extractDataStream(event);
                        yield this.agent.dwn.node.processMessage(did, event.message, { dataStream });
                    }
                    catch (error) {
                        console.error(`SyncEngineLevel: Error processing live-pull event for ${did}`, error);
                    }
                    // Persist cursor for resume on reconnect.
                    yield this.setCursor(cursorKey, subMessage.cursor);
                }
            });
            // Send the subscription request through the agent's DWN API.
            const response = yield this.agent.dwn.sendRequest({
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSubscribe,
                granteeDid: delegateDid,
                messageParams: {
                    filters,
                    cursor,
                    permissionGrantId,
                },
                subscriptionHandler: subscriptionHandler,
            });
            const reply = response.reply;
            if (reply.status.code !== 200 || !reply.subscription) {
                console.error(`SyncEngineLevel: MessagesSubscribe failed for ${did} -> ${dwnUrl}: ${reply.status.code} ${reply.status.detail}`);
                return;
            }
            this._liveSubscriptions.push({
                did,
                dwnUrl,
                delegateDid,
                protocol,
                close: () => __awaiter(this, void 0, void 0, function* () { yield reply.subscription.close(); }),
            });
            this._connectivityState = 'online';
        });
    }
    // ---------------------------------------------------------------------------
    // Live push: local EventLog subscription for immediate push
    // ---------------------------------------------------------------------------
    /**
     * Subscribes to the local DWN's EventLog so that writes by the user are
     * immediately pushed to the remote DWN instead of waiting for the next poll.
     */
    openLocalPushSubscription(target) {
        return __awaiter(this, void 0, void 0, function* () {
            const { did, delegateDid, dwnUrl, protocol } = target;
            // Build filters scoped to the protocol (if any).
            const filters = protocol ? [{ protocol }] : [];
            // Look up permission grant for local subscription.
            let permissionGrantId;
            if (delegateDid) {
                try {
                    const grant = yield this._permissionsApi.getPermissionForRequest({
                        connectedDid: did,
                        messageType: DwnInterface.MessagesRead,
                        delegateDid,
                        protocol,
                        cached: true,
                    });
                    permissionGrantId = grant.grant.id;
                }
                catch (_a) {
                    // No grant available — skip push-on-write for this target.
                    return;
                }
            }
            // Subscribe to the local DWN's EventLog.
            const subscriptionHandler = (subMessage) => {
                if (subMessage.type !== 'event') {
                    return;
                }
                // Accumulate the message CID for a debounced push.
                const targetKey = this.buildCursorKey(did, dwnUrl, protocol);
                const cid = this.tryGetCidSync(subMessage.event.message);
                if (cid === undefined) {
                    return;
                }
                let pending = this._pendingPushCids.get(targetKey);
                if (!pending) {
                    pending = { did, dwnUrl, delegateDid, protocol, cids: [] };
                    this._pendingPushCids.set(targetKey, pending);
                }
                pending.cids.push(cid);
                // Debounce the push.
                if (this._pushDebounceTimer) {
                    clearTimeout(this._pushDebounceTimer);
                }
                this._pushDebounceTimer = setTimeout(() => {
                    void this.flushPendingPushes();
                }, PUSH_DEBOUNCE_MS);
            };
            // Process the local subscription request.
            const response = yield this.agent.dwn.processRequest({
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSubscribe,
                granteeDid: delegateDid,
                messageParams: { filters, permissionGrantId },
                subscriptionHandler: subscriptionHandler,
            });
            const reply = response.reply;
            if (reply.status.code !== 200 || !reply.subscription) {
                console.error(`SyncEngineLevel: Local MessagesSubscribe failed for ${did}: ${reply.status.code} ${reply.status.detail}`);
                return;
            }
            this._localSubscriptions.push({
                did,
                dwnUrl,
                delegateDid,
                protocol,
                close: () => __awaiter(this, void 0, void 0, function* () { yield reply.subscription.close(); }),
            });
        });
    }
    /**
     * Flushes accumulated push CIDs to remote DWNs.
     */
    flushPendingPushes() {
        return __awaiter(this, void 0, void 0, function* () {
            this._pushDebounceTimer = undefined;
            const entries = [...this._pendingPushCids.entries()];
            this._pendingPushCids.clear();
            for (const [, pending] of entries) {
                const { did, dwnUrl, delegateDid, protocol, cids } = pending;
                if (cids.length === 0) {
                    continue;
                }
                try {
                    yield pushMessages({
                        did, dwnUrl, delegateDid, protocol,
                        messageCids: cids,
                        agent: this.agent,
                        permissionsApi: this._permissionsApi,
                    });
                }
                catch (error) {
                    console.error(`SyncEngineLevel: Push-on-write failed for ${did} -> ${dwnUrl}`, error);
                }
            }
        });
    }
    // ---------------------------------------------------------------------------
    // Cursor persistence
    // ---------------------------------------------------------------------------
    buildCursorKey(did, dwnUrl, protocol) {
        const base = `${did}${CURSOR_SEPARATOR}${dwnUrl}`;
        return protocol ? `${base}${CURSOR_SEPARATOR}${protocol}` : base;
    }
    getCursor(key) {
        return __awaiter(this, void 0, void 0, function* () {
            const cursors = this._db.sublevel('syncCursors');
            try {
                return yield cursors.get(key);
            }
            catch (error) {
                const e = error;
                if (e.code === 'LEVEL_NOT_FOUND') {
                    return undefined;
                }
                throw error;
            }
        });
    }
    setCursor(key, cursor) {
        return __awaiter(this, void 0, void 0, function* () {
            const cursors = this._db.sublevel('syncCursors');
            yield cursors.put(key, cursor);
        });
    }
    // ---------------------------------------------------------------------------
    // Utility helpers
    // ---------------------------------------------------------------------------
    /**
     * Extracts a ReadableStream from a MessageEvent if it contains a RecordsWrite with data.
     */
    extractDataStream(event) {
        if (isRecordsWrite(event) && event.data) {
            return event.data;
        }
        return undefined;
    }
    /**
     * Synchronously attempts to get a message CID. Returns undefined on failure.
     * This is used in the synchronous EventLog callback; the actual CID computation
     * is fast for already-constructed messages.
     */
    tryGetCidSync(message) {
        var _a;
        // Message.getCid is async but very fast (SHA-256 of the descriptor).
        // We fire-and-forget into a microtask and store the result.
        // For the debounced push, the CID will be resolved by the time we flush.
        let cid;
        void Message.getCid(message).then((result) => { cid = result; });
        // Since this is a microtask, it may not resolve immediately.
        // Use the descriptor's CID field if available as a synchronous fallback.
        return (_a = cid !== null && cid !== void 0 ? cid : message.messageCid) !== null && _a !== void 0 ? _a : undefined;
    }
    // ---------------------------------------------------------------------------
    // Default Hash Cache
    // ---------------------------------------------------------------------------
    /**
     * Returns the hex-encoded default (empty-subtree) hash for a given depth.
     * Lazily initializes the cache on first call.
     */
    getDefaultHashHex(depth) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            if (this._defaultHashHex === undefined) {
                const defaults = yield initDefaultHashes();
                const map = new Map();
                // Pre-compute hex strings for depths 0 through MAX_DIFF_DEPTH (inclusive).
                for (let d = 0; d <= MAX_DIFF_DEPTH; d++) {
                    map.set(d, hashToHex(defaults[d]));
                }
                this._defaultHashHex = map;
            }
            return (_a = this._defaultHashHex.get(depth)) !== null && _a !== void 0 ? _a : '';
        });
    }
    // ---------------------------------------------------------------------------
    // SMT Root Comparison
    // ---------------------------------------------------------------------------
    /**
     * Get the SMT root hash from the local DWN via a MessagesSync 'root' action.
     * Returns a hex-encoded root hash string.
     */
    getLocalRoot(did, delegateDid, protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const permissionGrantId = yield this.getSyncPermissionGrantId(did, delegateDid, protocol);
            const response = yield this.agent.dwn.processRequest({
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'root',
                    protocol,
                    permissionGrantId
                }
            });
            const reply = response.reply;
            return (_a = reply.root) !== null && _a !== void 0 ? _a : '';
        });
    }
    /**
     * Get the SMT root hash from a remote DWN via a MessagesSync 'root' action.
     * Returns a hex-encoded root hash string.
     */
    getRemoteRoot(did, dwnUrl, delegateDid, protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const permissionGrantId = yield this.getSyncPermissionGrantId(did, delegateDid, protocol);
            const syncMessage = yield this.agent.dwn.processRequest({
                store: false,
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'root',
                    protocol,
                    permissionGrantId
                }
            });
            const reply = yield this.agent.rpc.sendDwnRequest({
                dwnUrl,
                targetDid: did,
                message: syncMessage.message,
            });
            return (_a = reply.root) !== null && _a !== void 0 ? _a : '';
        });
    }
    // ---------------------------------------------------------------------------
    // Tree Diff — walk the SMT to find divergent leaf sets
    // ---------------------------------------------------------------------------
    /**
     * Walks the local and remote SMTs in parallel, recursing into subtrees whose
     * hashes differ, until reaching `MAX_DIFF_DEPTH` where leaves are enumerated.
     *
     * Returns the sets of messageCids that exist only locally or only remotely.
     */
    walkTreeDiff(_a) {
        return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol }) {
            const onlyLocal = [];
            const onlyRemote = [];
            // Hoist permission grant lookup — resolved once and reused for all subtree/leaf requests.
            const permissionGrantId = yield this.getSyncPermissionGrantId(did, delegateDid, protocol);
            const walk = (prefix) => __awaiter(this, void 0, void 0, function* () {
                // Get subtree hashes for this prefix from local and remote.
                const [localHash, remoteHash] = yield Promise.all([
                    this.getLocalSubtreeHash(did, prefix, delegateDid, protocol, permissionGrantId),
                    this.getRemoteSubtreeHash(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId),
                ]);
                // If hashes match, this subtree is identical — skip.
                if (localHash === remoteHash) {
                    return;
                }
                // Short-circuit: if one side is the default (empty-subtree) hash, all entries
                // on the other side are unique.  Enumerate leaves directly instead of recursing
                // further into the tree — this avoids an exponential walk when one DWN has
                // entries that the other lacks entirely in this subtree.
                const emptyHash = yield this.getDefaultHashHex(prefix.length);
                if (remoteHash === emptyHash && localHash !== emptyHash) {
                    const localLeaves = yield this.getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantId);
                    onlyLocal.push(...localLeaves);
                    return;
                }
                if (localHash === emptyHash && remoteHash !== emptyHash) {
                    const remoteLeaves = yield this.getRemoteLeaves(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId);
                    onlyRemote.push(...remoteLeaves);
                    return;
                }
                // If we've reached the maximum diff depth, enumerate leaves.
                if (prefix.length >= MAX_DIFF_DEPTH) {
                    const [localLeaves, remoteLeaves] = yield Promise.all([
                        this.getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantId),
                        this.getRemoteLeaves(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId),
                    ]);
                    const localSet = new Set(localLeaves);
                    const remoteSet = new Set(remoteLeaves);
                    for (const cid of localLeaves) {
                        if (!remoteSet.has(cid)) {
                            onlyLocal.push(cid);
                        }
                    }
                    for (const cid of remoteLeaves) {
                        if (!localSet.has(cid)) {
                            onlyRemote.push(cid);
                        }
                    }
                    return;
                }
                // Recurse into left (0) and right (1) children in parallel.
                yield Promise.all([
                    walk(prefix + '0'),
                    walk(prefix + '1'),
                ]);
            });
            yield walk('');
            return { onlyLocal, onlyRemote };
        });
    }
    getLocalSubtreeHash(did, prefix, delegateDid, protocol, permissionGrantId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const response = yield this.agent.dwn.processRequest({
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'subtree',
                    prefix,
                    protocol,
                    permissionGrantId
                }
            });
            const reply = response.reply;
            return (_a = reply.hash) !== null && _a !== void 0 ? _a : '';
        });
    }
    getRemoteSubtreeHash(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const syncMessage = yield this.agent.dwn.processRequest({
                store: false,
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'subtree',
                    prefix,
                    protocol,
                    permissionGrantId
                }
            });
            const reply = yield this.agent.rpc.sendDwnRequest({
                dwnUrl,
                targetDid: did,
                message: syncMessage.message,
            });
            return (_a = reply.hash) !== null && _a !== void 0 ? _a : '';
        });
    }
    getLocalLeaves(did, prefix, delegateDid, protocol, permissionGrantId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const response = yield this.agent.dwn.processRequest({
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'leaves',
                    prefix,
                    protocol,
                    permissionGrantId
                }
            });
            const reply = response.reply;
            return (_a = reply.entries) !== null && _a !== void 0 ? _a : [];
        });
    }
    getRemoteLeaves(did, dwnUrl, prefix, delegateDid, protocol, permissionGrantId) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const syncMessage = yield this.agent.dwn.processRequest({
                store: false,
                author: did,
                target: did,
                messageType: DwnInterface.MessagesSync,
                granteeDid: delegateDid,
                messageParams: {
                    action: 'leaves',
                    prefix,
                    protocol,
                    permissionGrantId
                }
            });
            const reply = yield this.agent.rpc.sendDwnRequest({
                dwnUrl,
                targetDid: did,
                message: syncMessage.message,
            });
            return (_a = reply.entries) !== null && _a !== void 0 ? _a : [];
        });
    }
    // ---------------------------------------------------------------------------
    // Pull / Push — delegates to standalone functions in sync-messages.ts
    // ---------------------------------------------------------------------------
    /**
     * Fetches missing messages from the remote DWN and processes them locally
     * in dependency order (topological sort).
     */
    pullMessages(_a) {
        return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids }) {
            return pullMessages({
                did, dwnUrl, delegateDid, protocol, messageCids,
                agent: this.agent,
                permissionsApi: this._permissionsApi,
            });
        });
    }
    /**
     * Reads missing messages from the local DWN and pushes them to the remote DWN
     * in dependency order (topological sort).
     */
    pushMessages(_a) {
        return __awaiter(this, arguments, void 0, function* ({ did, dwnUrl, delegateDid, protocol, messageCids }) {
            return pushMessages({
                did, dwnUrl, delegateDid, protocol, messageCids,
                agent: this.agent,
                permissionsApi: this._permissionsApi,
            });
        });
    }
    // ---------------------------------------------------------------------------
    // Dependency-aware topological sort — delegates to sync-topological-sort.ts
    // ---------------------------------------------------------------------------
    /**
     * Delegate to the standalone `topologicalSort` function.
     * Tests call `SyncEngineLevel.topologicalSort(...)` so this static method must remain.
     */
    static topologicalSort(messages) {
        return topologicalSort(messages);
    }
    /**
     * Returns the list of sync targets: (did, dwnUrl, delegateDid?, protocol?) tuples.
     */
    getSyncTargets() {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, e_1, _b, _c;
            const targets = [];
            try {
                for (var _d = true, _e = __asyncValues(this._db.sublevel('registeredIdentities').iterator()), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
                    _c = _f.value;
                    _d = false;
                    const [did, options] = _c;
                    let parsed;
                    try {
                        parsed = JSON.parse(options);
                    }
                    catch (_g) {
                        parsed = { protocols: [] };
                    }
                    const { protocols, delegateDid } = parsed;
                    const dwnEndpointUrls = yield getDwnServiceEndpointUrls(did, this.agent.did);
                    if (dwnEndpointUrls.length === 0) {
                        continue;
                    }
                    for (const dwnUrl of dwnEndpointUrls) {
                        if (protocols.length === 0) {
                            // Sync all protocols (global tree).
                            targets.push({ did, delegateDid, dwnUrl });
                        }
                        else {
                            for (const protocol of protocols) {
                                targets.push({ did, delegateDid, dwnUrl, protocol });
                            }
                        }
                    }
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
                }
                finally { if (e_1) throw e_1.error; }
            }
            return targets;
        });
    }
    /**
     * Gets the permission grant ID for MessagesSync if a delegateDid is provided.
     * Returns undefined if no delegate is in use (owner access).
     */
    getSyncPermissionGrantId(did, delegateDid, protocol) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!delegateDid) {
                return undefined;
            }
            try {
                const messagesSyncGrant = yield this._permissionsApi.getPermissionForRequest({
                    connectedDid: did,
                    messageType: DwnInterface.MessagesSync,
                    delegateDid,
                    protocol,
                    cached: true
                });
                return messagesSyncGrant.grant.id;
            }
            catch (error) {
                console.error('SyncEngineLevel: Error fetching MessagesSync permission grant for delegate DID', error);
                return undefined;
            }
        });
    }
}
/** Maximum consecutive failures before entering backoff. */
SyncEngineLevel.MAX_CONSECUTIVE_FAILURES = 5;
/** Backoff multiplier for consecutive failures (caps at 4x the configured interval). */
SyncEngineLevel.MAX_BACKOFF_MULTIPLIER = 4;
//# sourceMappingURL=sync-engine-level.js.map