import { authenticate } from '../core/auth.js';
import { DateSort } from '../types/records-types.js';
import { Message } from '../core/message.js';
import { messageReplyFromError } from '../core/message-reply.js';
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
import { Records } from '../utils/records.js';
import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
import { RecordsSubscribe } from '../interfaces/records-subscribe.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { SortDirection } from '../types/query-types.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { isRecordLimitOccupant, queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
export class RecordsSubscribeHandler {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    async handle({ tenant, message, subscriptionHandler, }) {
        if (this.deps.eventLog === undefined) {
            return messageReplyFromError(new DwnError(DwnErrorCode.RecordsSubscribeEventLogUnimplemented, 'Subscriptions are not supported'), 501);
        }
        let recordsSubscribe;
        try {
            recordsSubscribe = await RecordsSubscribe.parse(message);
        }
        catch (e) {
            return messageReplyFromError(e, 400);
        }
        let eventFilters = [];
        let queryFilters = [];
        // if this is an anonymous subscribe and the filter supports published records, subscribe to only published records
        if (Records.filterIncludesPublishedRecords(recordsSubscribe.message.descriptor.filter) && recordsSubscribe.author === undefined) {
            eventFilters = [RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe)];
            queryFilters = [RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe)];
            // delete the undefined authorization property else the code will encounter the following IPLD issue when attempting to generate CID:
            // Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded
            delete message.authorization;
        }
        else {
            // authentication and authorization
            try {
                await authenticate(message.authorization, this.deps.didResolver);
                await RecordsSubscribeHandler.authorizeRecordsSubscribe(tenant, recordsSubscribe, this.deps);
            }
            catch (error) {
                return messageReplyFromError(error, 401);
            }
            if (recordsSubscribe.author === tenant) {
                eventFilters = RecordsSubscribeHandler.buildOwnerEventFilters(recordsSubscribe);
                queryFilters = RecordsSubscribeHandler.buildOwnerQueryFilters(recordsSubscribe);
            }
            else {
                eventFilters = RecordsSubscribeHandler.buildNonOwnerEventFilters(recordsSubscribe);
                queryFilters = RecordsSubscribeHandler.buildNonOwnerQueryFilters(recordsSubscribe);
            }
        }
        const messageCid = await Message.getCid(message);
        const { cursor: eventLogCursor } = recordsSubscribe.message.descriptor;
        const projectedSubscriptionHandler = RecordsSubscribeHandler.createRecordLimitOccupancyGuard({
            deps: this.deps,
            recordsSubscribe,
            subscriptionHandler,
            tenant,
        });
        if (eventLogCursor !== undefined) {
            // ---- Cursor mode: catch-up from EventLog + EOSE + live ----
            // All catch-up, buffering, dedup, and EOSE delivery are handled by the
            // EventLog implementation. The handler just passes the cursor and filters.
            // The subscriptionHandler receives SubscriptionMessage (event + EOSE) directly.
            try {
                const subscription = await this.deps.eventLog.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
                    cursor: eventLogCursor,
                    filters: eventFilters,
                });
                await projectedSubscriptionHandler.setSubscription(subscription);
                return {
                    status: { code: 200, detail: 'OK' },
                    subscription,
                };
            }
            catch (error) {
                if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
                    const gapInfo = error.gapInfo;
                    return {
                        status: { code: 410, detail: 'Progress token gap' },
                        error: gapInfo === undefined ? undefined : { code: 'ProgressGap', ...gapInfo },
                    };
                }
                return messageReplyFromError(error, 500);
            }
        }
        // ---- No cursor: existing behavior (initial snapshot from MessageStore) ----
        // Step 1: Register event listener FIRST to ensure no events are missed between query and subscribe
        const subscription = await this.deps.eventLog.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
            filters: eventFilters,
        });
        await projectedSubscriptionHandler.setSubscription(subscription);
        // Step 2: Query for initial snapshot of matching records
        let entries;
        let paginationCursor;
        try {
            const { dateSort, pagination } = recordsSubscribe.message.descriptor;
            const messageSort = RecordsSubscribeHandler.convertDateSort(dateSort);
            const queryResult = await queryRecordsWithRecordLimitOccupancy({
                messageStore: this.deps.messageStore,
                validationStateReader: this.deps.validationStateReader,
                tenant,
                filters: queryFilters,
                messageSort,
                pagination,
                messageTimestamp: recordsSubscribe.message.descriptor.messageTimestamp,
            });
            entries = queryResult.messages;
            paginationCursor = queryResult.cursor;
            // attach initialWrite for non-initial writes
            for (const entry of entries) {
                if (!await RecordsWrite.isInitialWrite(entry)) {
                    const initialWriteResult = await this.deps.messageStore.query(tenant, [{ recordId: entry.recordId, isLatestBaseState: false, method: DwnMethodName.Write }]);
                    const initialWrite = initialWriteResult.messages[0];
                    delete initialWrite.encodedData;
                    entry.initialWrite = initialWrite;
                }
            }
        }
        catch (error) {
            // if the query fails, close the subscription and return the error
            await subscription.close();
            return messageReplyFromError(error, 500);
        }
        // Step 3: Return subscription + initial entries + cursor
        return {
            status: { code: 200, detail: 'OK' },
            subscription,
            entries,
            cursor: paginationCursor,
        };
    }
    /**
     * Convert an incoming DateSort to a sort type accepted by MessageStore.
     * Defaults to `dateCreated` ascending if no sort is supplied.
     */
    static convertDateSort(dateSort) {
        switch (dateSort) {
            case DateSort.CreatedAscending:
                return { dateCreated: SortDirection.Ascending };
            case DateSort.CreatedDescending:
                return { dateCreated: SortDirection.Descending };
            case DateSort.PublishedAscending:
                return { datePublished: SortDirection.Ascending };
            case DateSort.PublishedDescending:
                return { datePublished: SortDirection.Descending };
            case DateSort.UpdatedAscending:
                return { messageTimestamp: SortDirection.Ascending };
            case DateSort.UpdatedDescending:
                return { messageTimestamp: SortDirection.Descending };
            default:
                return { dateCreated: SortDirection.Ascending };
        }
    }
    static createRecordLimitOccupancyGuard(input) {
        const { deps, recordsSubscribe, subscriptionHandler, tenant } = input;
        let subscription;
        let closeRequested = false;
        let terminalErrorEmitted = false;
        let deliveryQueue = Promise.resolve();
        const closeSubscription = () => {
            if (closeRequested) {
                return;
            }
            closeRequested = true;
            Promise.resolve(subscription?.close()).catch(() => { });
        };
        const emitTerminalProjectionError = (cursor) => {
            if (terminalErrorEmitted) {
                return;
            }
            terminalErrorEmitted = true;
            subscriptionHandler({
                type: 'error',
                cursor,
                error: {
                    code: 'RecordsSubscribeProjectionFailed',
                    detail: 'record-limit occupancy projection failed during delivery',
                },
            });
        };
        const deliverProjectedEvent = async (subscriptionEvent) => {
            const { message } = subscriptionEvent.event;
            if (Records.isRecordsWrite(message)) {
                let isOccupant;
                try {
                    isOccupant = await isRecordLimitOccupant({
                        messageStore: deps.messageStore,
                        validationStateReader: deps.validationStateReader,
                        tenant,
                        message,
                        messageTimestamp: recordsSubscribe.message.descriptor.messageTimestamp,
                    });
                }
                catch {
                    emitTerminalProjectionError(subscriptionEvent.cursor);
                    closeSubscription();
                    return;
                }
                if (!isOccupant) {
                    return;
                }
            }
            if (!closeRequested) {
                subscriptionHandler(subscriptionEvent);
            }
        };
        const deliverQueuedMessage = async (subscriptionMessage) => {
            if (closeRequested) {
                return;
            }
            if (subscriptionMessage.type !== 'event') {
                subscriptionHandler(subscriptionMessage);
                return;
            }
            await deliverProjectedEvent(subscriptionMessage);
        };
        const enqueueDelivery = (subscriptionMessage) => {
            deliveryQueue = deliveryQueue
                .then(() => deliverQueuedMessage(subscriptionMessage))
                .catch(() => { });
        };
        return {
            listener: (subscriptionMessage) => {
                enqueueDelivery(subscriptionMessage);
            },
            setSubscription: async (eventSubscription) => {
                subscription = eventSubscription;
                if (closeRequested) {
                    await eventSubscription.close();
                }
            },
        };
    }
    // =============================================
    // Event filters (for live subscription)
    // These match Write+Delete and do NOT use isLatestBaseState
    // =============================================
    /**
     * Build event filters for owner: all matching Write+Delete events.
     */
    static buildOwnerEventFilters(recordsSubscribe) {
        const { filter } = recordsSubscribe.message.descriptor;
        return [{
                ...Records.convertFilter(filter),
                interface: DwnInterfaceName.Records,
                method: [DwnMethodName.Write, DwnMethodName.Delete],
            }];
    }
    /**
     * Build event filters for non-owner with visibility rules.
     */
    static buildNonOwnerEventFilters(recordsSubscribe) {
        const filters = [];
        const { filter } = recordsSubscribe.message.descriptor;
        if (Records.filterIncludesPublishedRecords(filter)) {
            filters.push(RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe));
        }
        if (Records.filterIncludesUnpublishedRecords(filter)) {
            if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author)) {
                filters.push({
                    ...Records.convertFilter(filter),
                    author: recordsSubscribe.author,
                    interface: DwnInterfaceName.Records,
                    method: [DwnMethodName.Write, DwnMethodName.Delete],
                    published: false,
                });
            }
            if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload)) {
                filters.push({
                    ...Records.convertFilter(filter),
                    interface: DwnInterfaceName.Records,
                    method: [DwnMethodName.Write, DwnMethodName.Delete],
                    published: false,
                });
            }
            if (Message.getPermissionGrantId(recordsSubscribe.signaturePayload) !== undefined) {
                filters.push({
                    ...Records.convertFilter(filter),
                    interface: DwnInterfaceName.Records,
                    method: [DwnMethodName.Write, DwnMethodName.Delete],
                    published: false,
                });
            }
            if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author)) {
                filters.push({
                    ...Records.convertFilter(filter),
                    interface: DwnInterfaceName.Records,
                    method: [DwnMethodName.Write, DwnMethodName.Delete],
                    recipient: recordsSubscribe.author,
                    published: false,
                });
            }
        }
        return filters;
    }
    /**
     * Build a published-only event filter (Write+Delete).
     */
    static buildPublishedEventFilter(recordsSubscribe) {
        return {
            ...Records.convertFilter(recordsSubscribe.message.descriptor.filter),
            interface: DwnInterfaceName.Records,
            method: [DwnMethodName.Write, DwnMethodName.Delete],
            published: true,
        };
    }
    // =============================================
    // Query filters (for initial snapshot)
    // These match Write only and use isLatestBaseState: true
    // =============================================
    /**
     * Build query filters for owner: latest writes matching the filter.
     */
    static buildOwnerQueryFilters(recordsSubscribe) {
        const { dateSort, filter } = recordsSubscribe.message.descriptor;
        return [{
                ...Records.convertFilter(filter, dateSort),
                interface: DwnInterfaceName.Records,
                method: DwnMethodName.Write,
                isLatestBaseState: true,
            }];
    }
    /**
     * Build query filters for non-owner with visibility rules.
     */
    static buildNonOwnerQueryFilters(recordsSubscribe) {
        const filters = [];
        const { dateSort, filter } = recordsSubscribe.message.descriptor;
        if (Records.filterIncludesPublishedRecords(filter)) {
            filters.push(RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe));
        }
        if (Records.filterIncludesUnpublishedRecords(filter)) {
            if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author)) {
                filters.push({
                    ...Records.convertFilter(filter, dateSort),
                    author: recordsSubscribe.author,
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    isLatestBaseState: true,
                    published: false,
                });
            }
            if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload)) {
                filters.push({
                    ...Records.convertFilter(filter, dateSort),
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    isLatestBaseState: true,
                    published: false,
                });
            }
            if (Message.getPermissionGrantId(recordsSubscribe.signaturePayload) !== undefined) {
                filters.push({
                    ...Records.convertFilter(filter, dateSort),
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    isLatestBaseState: true,
                    published: false,
                });
            }
            if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author)) {
                filters.push({
                    ...Records.convertFilter(filter, dateSort),
                    interface: DwnInterfaceName.Records,
                    method: DwnMethodName.Write,
                    recipient: recordsSubscribe.author,
                    isLatestBaseState: true,
                    published: false,
                });
            }
        }
        return filters;
    }
    /**
     * Build a published-only query filter (latest writes).
     */
    static buildPublishedQueryFilter(recordsSubscribe) {
        const { dateSort, filter } = recordsSubscribe.message.descriptor;
        return {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            published: true,
            isLatestBaseState: true,
        };
    }
    /**
     * @param messageStore Used to check if the grant has been revoked.
     */
    static async authorizeRecordsSubscribe(tenant, recordsSubscribe, deps) {
        if (Message.isSignedByAuthorDelegate(recordsSubscribe.message)) {
            await recordsSubscribe.authorizeDelegate(deps.validationStateReader);
        }
        const permissionGrantId = Message.getPermissionGrantId(recordsSubscribe.signaturePayload);
        if (permissionGrantId !== undefined) {
            const permissionGrant = await deps.validationStateReader.fetchGrant(tenant, permissionGrantId);
            await RecordsGrantAuthorization.authorizeQueryOrSubscribe({
                incomingMessage: recordsSubscribe.message,
                expectedGrantor: tenant,
                expectedGrantee: recordsSubscribe.author,
                permissionGrant,
                validationStateReader: deps.validationStateReader,
            });
            return;
        }
        // NOTE: not all RecordsSubscribe messages require protocol authorization even if the filter includes protocol-related fields,
        // this is because we dynamically filter out records that the caller is not authorized to see.
        // Currently only run protocol authorization if message deliberately invokes a protocol role.
        if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload)) {
            await ProtocolAuthorization.authorizeQueryOrSubscribe(tenant, recordsSubscribe, deps.validationStateReader);
        }
    }
}
//# sourceMappingURL=records-subscribe.js.map