import { Messages } from '../utils/messages.js';
import { Records } from '../utils/records.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { Replication } from '../utils/replication.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
const DEFAULT_READ_LIMIT = 100;
const DEFAULT_IDLE_REDRAIN_INTERVAL_MS = 30_000;
/**
 * EventLog implementation backed by the durable replication feed.
 *
 * Writes are store-owned: MessageStore commits publish wakes after the row is
 * durable.
 */
export class DurableEventLog {
    store;
    wakeSubscriber;
    subscriptions = new Map();
    readLimit;
    idleRedrainIntervalMs;
    errorHandler;
    unsubscribeWake;
    idleRedrainTimer;
    isOpen = false;
    constructor(store, wakeSubscriber, config = {}) {
        this.store = store;
        this.wakeSubscriber = wakeSubscriber;
        this.readLimit = Math.max(1, config.readLimit ?? DEFAULT_READ_LIMIT);
        this.idleRedrainIntervalMs = config.idleRedrainIntervalMs ?? DEFAULT_IDLE_REDRAIN_INTERVAL_MS;
        this.errorHandler = config.errorHandler ?? ((error) => { console.error('durable event log error', error); });
    }
    async open() {
        if (this.isOpen) {
            return;
        }
        this.unsubscribeWake = this.wakeSubscriber.subscribe((wake) => {
            this.handleWake(wake);
        });
        if (this.idleRedrainIntervalMs > 0) {
            this.idleRedrainTimer = setInterval(() => {
                this.redrainAll();
            }, this.idleRedrainIntervalMs);
        }
        this.isOpen = true;
    }
    async close() {
        this.unsubscribeWake?.();
        this.unsubscribeWake = undefined;
        if (this.idleRedrainTimer !== undefined) {
            clearInterval(this.idleRedrainTimer);
            this.idleRedrainTimer = undefined;
        }
        for (const subscription of this.subscriptions.values()) {
            subscription.closed = true;
        }
        this.subscriptions.clear();
        this.isOpen = false;
    }
    async read(tenant, options = {}) {
        const result = await this.store.logRead(tenant, options);
        return {
            ...result,
            events: DurableEventLog.detachEntryEncodedData(await this.attachInitialWrites(tenant, result.events)),
        };
    }
    async subscribe(tenant, id, listener, options = {}) {
        const subscription = {
            id,
            tenant,
            listener,
            filters: options.filters,
            cursor: await this.getInitialCursor(tenant, options.cursor),
            closed: false,
            draining: false,
            liveReady: options.cursor === undefined,
            redrainRequested: false,
        };
        this.subscriptions.set(id, subscription);
        if (options.cursor !== undefined) {
            try {
                const eoseCursor = await this.catchUpSubscription(subscription, options.cursor);
                if (!subscription.closed) {
                    listener({ type: 'eose', cursor: eoseCursor });
                    subscription.liveReady = true;
                    if (subscription.redrainRequested) {
                        this.scheduleDrain(subscription);
                    }
                }
            }
            catch (error) {
                subscription.closed = true;
                this.subscriptions.delete(id);
                throw error;
            }
        }
        return {
            id,
            close: async () => {
                subscription.closed = true;
                this.subscriptions.delete(id);
            }
        };
    }
    async getReplayBounds(tenant) {
        return this.store.logBounds(tenant);
    }
    async getInitialCursor(tenant, cursor) {
        if (cursor !== undefined) {
            return cursor;
        }
        const bounds = await this.store.logBounds(tenant);
        return bounds?.latest;
    }
    handleWake(wake) {
        for (const subscription of this.subscriptions.values()) {
            if (subscription.tenant !== wake.tenant || subscription.closed) {
                continue;
            }
            this.scheduleDrain(subscription);
        }
    }
    redrainAll() {
        for (const subscription of this.subscriptions.values()) {
            if (subscription.closed) {
                continue;
            }
            this.scheduleDrain(subscription);
        }
    }
    scheduleDrain(subscription) {
        if (!subscription.liveReady) {
            subscription.redrainRequested = true;
            return;
        }
        void this.drainSubscription(subscription).catch((error) => {
            this.handleDrainError(subscription, error);
        });
    }
    handleDrainError(subscription, error) {
        if (subscription.closed) {
            return;
        }
        if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
            const gapInfo = DurableEventLog.getProgressGapInfo(error);
            const cursor = gapInfo?.requested ?? subscription.cursor ?? gapInfo?.latestAvailable;
            if (cursor === undefined) {
                this.errorHandler(error);
                return;
            }
            subscription.closed = true;
            this.subscriptions.delete(subscription.id);
            subscription.listener({
                type: 'error',
                cursor,
                error: {
                    code: 'ProgressGap',
                    detail: error.message,
                },
            });
            return;
        }
        this.errorHandler(error);
    }
    async catchUpSubscription(subscription, cursor) {
        const frozenCursor = await this.getCatchUpHighWater(subscription.tenant, cursor);
        const frozenPosition = BigInt(frozenCursor.position);
        let readCursor = cursor;
        subscription.cursor = cursor;
        while (!subscription.closed && BigInt(readCursor.position) < frozenPosition) {
            const result = await this.read(subscription.tenant, {
                cursor: readCursor,
                filters: subscription.filters,
                limit: this.readLimit,
            });
            const pageState = await this.deliverCatchUpPage(subscription, result.events, readCursor, frozenPosition);
            if (subscription.closed) {
                return frozenCursor;
            }
            const resultCursor = result.cursor ?? pageState.readCursor;
            if (DurableEventLog.isCatchUpComplete(pageState.reachedFrozenPosition, result, resultCursor, frozenPosition)) {
                DurableEventLog.finishCatchUp(subscription, frozenCursor);
                return frozenCursor;
            }
            readCursor = resultCursor;
            subscription.cursor = resultCursor;
        }
        DurableEventLog.finishCatchUp(subscription, frozenCursor);
        return frozenCursor;
    }
    async getCatchUpHighWater(tenant, cursor) {
        const bounds = await this.store.logBounds(tenant);
        const frozenCursor = bounds?.latest ?? cursor;
        await this.read(tenant, { cursor, limit: 0 });
        return frozenCursor;
    }
    async deliverCatchUpPage(subscription, entries, readCursor, frozenPosition) {
        let nextCursor = readCursor;
        for (const entry of entries) {
            if (BigInt(DurableEventLog.getEntryPosition(entry)) > frozenPosition) {
                return { readCursor: nextCursor, reachedFrozenPosition: true };
            }
            const deliveredCursor = await this.deliverEntry(subscription, entry);
            if (subscription.closed) {
                return { readCursor: nextCursor, reachedFrozenPosition: false };
            }
            nextCursor = deliveredCursor ?? nextCursor;
            subscription.cursor = nextCursor;
        }
        return { readCursor: nextCursor, reachedFrozenPosition: false };
    }
    async drainSubscription(subscription) {
        if (subscription.draining) {
            subscription.redrainRequested = true;
            return;
        }
        subscription.draining = true;
        try {
            do {
                subscription.redrainRequested = false;
                await this.drainOnce(subscription);
            } while (subscription.redrainRequested && !subscription.closed);
        }
        finally {
            subscription.draining = false;
        }
    }
    async drainOnce(subscription) {
        for (;;) {
            if (subscription.closed) {
                return;
            }
            const result = await this.read(subscription.tenant, {
                cursor: subscription.cursor,
                filters: subscription.filters,
                limit: this.readLimit,
            });
            for (const entry of result.events) {
                const cursor = await this.deliverEntry(subscription, entry);
                if (subscription.closed) {
                    return;
                }
                subscription.cursor = cursor ?? subscription.cursor;
            }
            if (subscription.closed) {
                return;
            }
            subscription.cursor = result.cursor ?? subscription.cursor;
            if (result.drained) {
                return;
            }
        }
    }
    async deliverEntry(subscription, entry) {
        if (subscription.closed) {
            return undefined;
        }
        const cursor = await this.buildToken(subscription.tenant, DurableEventLog.getEntryPosition(entry), entry.messageCid);
        if (subscription.closed) {
            return undefined;
        }
        const event = {
            type: 'event',
            cursor,
            event: entry.event,
            seq: entry.seq,
            messageCid: entry.messageCid,
            isLatestBaseState: DurableEventLog.isLatestBaseState(entry),
            protocol: DurableEventLog.getProtocol(entry),
        };
        if (entry.encodedData !== undefined) {
            event.encodedData = entry.encodedData;
        }
        subscription.listener(event);
        return cursor;
    }
    async attachInitialWrites(tenant, entries) {
        const result = [];
        for (const entry of entries) {
            result.push({
                ...entry,
                event: await this.attachInitialWrite(tenant, entry.event),
            });
        }
        return result;
    }
    async attachInitialWrite(tenant, event) {
        if (event.initialWrite !== undefined || !await DurableEventLog.needsInitialWrite(event.message)) {
            return event;
        }
        const recordId = DurableEventLog.getRecordId(event.message);
        if (recordId === undefined) {
            return event;
        }
        const { messages } = await this.store.query(tenant, [{ entryId: recordId }]);
        const initialWrite = messages[0];
        if (initialWrite === undefined) {
            return event;
        }
        const { message } = Messages.detachEncodedData(initialWrite);
        return { ...event, initialWrite: message };
    }
    static async needsInitialWrite(message) {
        if (Records.isRecordsWrite(message)) {
            return !await RecordsWrite.isInitialWrite(message);
        }
        return message.descriptor.interface === DwnInterfaceName.Records &&
            message.descriptor.method === DwnMethodName.Delete;
    }
    static getRecordId(message) {
        const recordId = message.recordId;
        if (typeof recordId === 'string') {
            return recordId;
        }
        const descriptorRecordId = message.descriptor.recordId;
        return typeof descriptorRecordId === 'string' ? descriptorRecordId : undefined;
    }
    async buildToken(tenant, position, messageCid) {
        const token = {
            streamId: await Replication.deriveStreamId(tenant),
            epoch: await this.store.epoch(),
            position,
        };
        if (messageCid !== undefined) {
            token.messageCid = messageCid;
        }
        return token;
    }
    static getEntryPosition(entry) {
        return entry.position ?? entry.seq;
    }
    static getProgressGapInfo(error) {
        const gapInfo = error.gapInfo;
        if (gapInfo === undefined) {
            return undefined;
        }
        return gapInfo;
    }
    static isCatchUpComplete(reachedFrozenPosition, result, resultCursor, frozenPosition) {
        return reachedFrozenPosition || result.drained || BigInt(resultCursor.position) >= frozenPosition;
    }
    static finishCatchUp(subscription, frozenCursor) {
        subscription.cursor = frozenCursor;
    }
    static isLatestBaseState(entry) {
        return entry.indexes.isLatestBaseState === true || entry.indexes.isLatestBaseState === 'true';
    }
    static getProtocol(entry) {
        const protocol = entry.indexes.protocol;
        return typeof protocol === 'string' ? protocol : undefined;
    }
    static detachEntryEncodedData(entries) {
        return entries.map((entry) => {
            const { message, encodedData } = Messages.detachEncodedData(entry.event.message);
            return {
                ...entry,
                event: {
                    ...entry.event,
                    message,
                },
                encodedData,
            };
        });
    }
}
//# sourceMappingURL=durable-event-log.js.map