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 { queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
import { Records } from '../utils/records.js';
import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
import { RecordsQuery } from '../interfaces/records-query.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { SortDirection } from '../types/query-types.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
export class RecordsQueryHandler {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    async handle({ tenant, message, }) {
        let recordsQuery;
        try {
            recordsQuery = await RecordsQuery.parse(message);
        }
        catch (e) {
            return messageReplyFromError(e, 400);
        }
        let recordsWrites;
        let cursor;
        // if this is an anonymous query and the filter supports published records, query only published records
        if (Records.filterIncludesPublishedRecords(recordsQuery.message.descriptor.filter) && recordsQuery.author === undefined) {
            const results = await this.fetchPublishedRecords(tenant, recordsQuery);
            recordsWrites = results.messages;
            cursor = results.cursor;
        }
        else {
            // authentication and authorization
            try {
                await authenticate(message.authorization, this.deps.didResolver);
                await RecordsQueryHandler.authorizeRecordsQuery(tenant, recordsQuery, this.deps);
            }
            catch (e) {
                return messageReplyFromError(e, 401);
            }
            if (recordsQuery.author === tenant) {
                const results = await this.fetchRecordsAsOwner(tenant, recordsQuery);
                recordsWrites = results.messages;
                cursor = results.cursor;
            }
            else {
                const results = await this.fetchRecordsAsNonOwner(tenant, recordsQuery);
                recordsWrites = results.messages;
                cursor = results.cursor;
            }
        }
        // attach initial write if returned RecordsWrite is not initial write
        for (const recordsWrite of recordsWrites) {
            if (!await RecordsWrite.isInitialWrite(recordsWrite)) {
                const initialWriteQueryResult = await this.deps.messageStore.query(tenant, [{ recordId: recordsWrite.recordId, isLatestBaseState: false, method: DwnMethodName.Write }]);
                const initialWrite = initialWriteQueryResult.messages[0];
                delete initialWrite.encodedData; // defensive measure but technically optional because we do this when an update RecordsWrite takes place
                recordsWrite.initialWrite = initialWrite;
            }
        }
        return {
            status: { code: 200, detail: 'OK' },
            entries: recordsWrites,
            cursor
        };
    }
    /**
     * Convert an incoming DateSort to a sort type accepted by MessageStore
     * Defaults to 'dateCreated' in Descending order if no sort is supplied.
     *
     * @param dateSort the optional DateSort from the RecordsQuery message descriptor.
     * @returns {MessageSort} for MessageStore sorting.
     */
    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 };
        }
    }
    /**
     * Fetches the records as the owner of the DWN with no additional filtering.
     */
    async fetchRecordsAsOwner(tenant, recordsQuery) {
        const { dateSort, filter, pagination } = recordsQuery.message.descriptor;
        // fetch all published records matching the query
        const queryFilter = {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true
        };
        const messageSort = this.convertDateSort(dateSort);
        return queryRecordsWithRecordLimitOccupancy({
            messageStore: this.deps.messageStore,
            validationStateReader: this.deps.validationStateReader,
            tenant,
            filters: [queryFilter],
            messageSort,
            pagination,
            messageTimestamp: recordsQuery.message.descriptor.messageTimestamp,
        });
    }
    /**
     * Fetches the records as a non-owner.
     *
     * Filters can support returning both published and unpublished records,
     * as well as explicitly only published or only unpublished records.
     *
     * A) BOTH published and unpublished:
     *    1. published records; and
     *    2. unpublished records intended for the query author (where `recipient` is the query author); and
     *    3. unpublished records authorized by a protocol rule.
     *
     * B) PUBLISHED:
     *    1. only published records;
     *
     * C) UNPUBLISHED:
     *    1. unpublished records intended for the query author (where `recipient` is the query author); and
     *    2. unpublished records authorized by a protocol rule.
     *
     */
    async fetchRecordsAsNonOwner(tenant, recordsQuery) {
        const { dateSort, pagination, filter } = recordsQuery.message.descriptor;
        const filters = [];
        if (Records.filterIncludesPublishedRecords(filter)) {
            filters.push(RecordsQueryHandler.buildPublishedRecordsFilter(recordsQuery));
        }
        if (Records.filterIncludesUnpublishedRecords(filter)) {
            if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsQuery.author)) {
                filters.push(RecordsQueryHandler.buildUnpublishedRecordsByQueryAuthorFilter(recordsQuery));
            }
            if (Records.shouldProtocolAuthorize(recordsQuery.signaturePayload)) {
                filters.push(RecordsQueryHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsQuery));
            }
            if (Message.getPermissionGrantId(recordsQuery.signaturePayload) !== undefined) {
                filters.push(RecordsQueryHandler.buildUnpublishedPermissionGrantAuthorizedRecordsFilter(recordsQuery));
            }
            if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsQuery.author)) {
                filters.push(RecordsQueryHandler.buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery));
            }
        }
        const messageSort = this.convertDateSort(dateSort);
        return queryRecordsWithRecordLimitOccupancy({
            messageStore: this.deps.messageStore,
            validationStateReader: this.deps.validationStateReader,
            tenant,
            filters,
            messageSort,
            pagination,
            messageTimestamp: recordsQuery.message.descriptor.messageTimestamp,
        });
    }
    /**
     * Fetches only published records.
     */
    async fetchPublishedRecords(tenant, recordsQuery) {
        const { dateSort, pagination } = recordsQuery.message.descriptor;
        const filter = RecordsQueryHandler.buildPublishedRecordsFilter(recordsQuery);
        const messageSort = this.convertDateSort(dateSort);
        return queryRecordsWithRecordLimitOccupancy({
            messageStore: this.deps.messageStore,
            validationStateReader: this.deps.validationStateReader,
            tenant,
            filters: [filter],
            messageSort,
            pagination,
            messageTimestamp: recordsQuery.message.descriptor.messageTimestamp,
        });
    }
    static buildPublishedRecordsFilter(recordsQuery) {
        const { dateSort, filter } = recordsQuery.message.descriptor;
        // fetch all published records matching the query
        return {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            published: true,
            isLatestBaseState: true
        };
    }
    /**
     * Creates a filter for unpublished records that are intended for the query author (where `recipient` is the author).
     */
    static buildUnpublishedRecordsForQueryAuthorFilter(recordsQuery) {
        const { dateSort, filter } = recordsQuery.message.descriptor;
        // include records where recipient is query author
        return {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            recipient: recordsQuery.author,
            isLatestBaseState: true,
            published: false
        };
    }
    /**
     * Creates a filter for unpublished records that are within the specified protocol.
     * Validation that `protocol` and other required protocol-related fields occurs before this method.
     */
    static buildUnpublishedProtocolAuthorizedRecordsFilter(recordsQuery) {
        const { dateSort, filter } = recordsQuery.message.descriptor;
        return {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            published: false
        };
    }
    /**
     * Creates a filter for unpublished records authorized by a permission grant.
     */
    static buildUnpublishedPermissionGrantAuthorizedRecordsFilter(recordsQuery) {
        const { dateSort, filter } = recordsQuery.message.descriptor;
        return {
            ...Records.convertFilter(filter, dateSort),
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            published: false
        };
    }
    /**
     * Creates a filter for only unpublished records where the author is the same as the query author.
     */
    static buildUnpublishedRecordsByQueryAuthorFilter(recordsQuery) {
        const { dateSort, filter } = recordsQuery.message.descriptor;
        // include records where author is the same as the query author
        return {
            ...Records.convertFilter(filter, dateSort),
            author: recordsQuery.author,
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            published: false
        };
    }
    /**
     * @param messageStore Used to check if the grant has been revoked.
     */
    static async authorizeRecordsQuery(tenant, recordsQuery, deps) {
        if (Message.isSignedByAuthorDelegate(recordsQuery.message)) {
            await recordsQuery.authorizeDelegate(deps.validationStateReader);
        }
        const permissionGrantId = Message.getPermissionGrantId(recordsQuery.signaturePayload);
        if (permissionGrantId !== undefined) {
            const permissionGrant = await deps.validationStateReader.fetchGrant(tenant, permissionGrantId);
            await RecordsGrantAuthorization.authorizeQueryOrSubscribe({
                incomingMessage: recordsQuery.message,
                expectedGrantor: tenant,
                expectedGrantee: recordsQuery.author,
                permissionGrant,
                validationStateReader: deps.validationStateReader,
            });
            return;
        }
        // NOTE: not all RecordsQuery 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(recordsQuery.signaturePayload)) {
            await ProtocolAuthorization.authorizeQueryOrSubscribe(tenant, recordsQuery, deps.validationStateReader);
        }
    }
}
//# sourceMappingURL=records-query.js.map