import { DwnConstant } from '../core/dwn-constant.js';
import { DwnMethodName } from '../enums/dwn-interface-method.js';
import { Encoder } from '../utils/encoder.js';
import { FilterUtility } from '../utils/filter.js';
import { Message } from '../core/message.js';
import { PermissionGrant } from './permission-grant.js';
import { PermissionRequest } from './permission-request.js';
import { PERMISSIONS_PROTOCOL_URI } from '../core/constants.js';
import { Records } from '../utils/records.js';
import { RecordsWrite } from '../interfaces/records-write.js';
import { Time } from '../utils/time.js';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../utils/url.js';
/**
 * This is a first-class DWN protocol for managing permission grants of a given DWN.
 *
 * It implements the `CoreProtocol` interface so that its lifecycle hooks
 * (validation, pre-processing, post-processing) are dispatched generically
 * by the `CoreProtocolRegistry` rather than being hardcoded in handlers.
 */
export class PermissionsProtocol {
    /**
     * The URI of the DWN Permissions protocol.
     */
    static uri = PERMISSIONS_PROTOCOL_URI;
    /**
     * The protocol path of the `request` record.
     */
    static requestPath = 'request';
    /**
     * The protocol path of the `grant` record.
     */
    static grantPath = 'grant';
    /**
     * The protocol path of the `revocation` record.
     */
    static revocationPath = 'grant/revocation';
    /**
     * Canonical store predicate for the latest revocation of a permission grant.
     */
    static grantRevocationFilter(grantId) {
        return {
            isLatestBaseState: true,
            parentId: grantId,
            protocolPath: PermissionsProtocol.revocationPath,
        };
    }
    /**
     * The definition of the Permissions protocol.
     */
    static definition = {
        published: true,
        protocol: PermissionsProtocol.uri,
        types: {
            request: {
                dataFormats: ['application/json']
            },
            grant: {
                dataFormats: ['application/json']
            },
            revocation: {
                dataFormats: ['application/json']
            }
        },
        structure: {
            // All permission record paths are `$immutable`: requests, grants, and revocations are
            // write-once by design (a grant is never amended — it is revoked and re-issued).
            // Immutability locks each record's initial-write facts (notably the `protocol` tag),
            // which replication fingerprint domains and protocol-scoped shadow filters depend on —
            // a tag-mutated permission record would otherwise drift between the shadow-filter
            // stream (current tags) and its fingerprint domain (initial tags).
            request: {
                $immutable: true,
                $size: {
                    max: 10000
                },
                $actions: [
                    {
                        who: 'anyone',
                        can: ['create']
                    }
                ]
            },
            grant: {
                $immutable: true,
                $size: {
                    max: 10000
                },
                $actions: [
                    {
                        who: 'recipient',
                        of: 'grant',
                        can: ['read']
                    }
                ],
                revocation: {
                    $immutable: true,
                    $size: {
                        max: 10000
                    },
                    $actions: [
                        {
                            who: 'anyone',
                            can: ['read']
                        }
                    ]
                }
            }
        }
    };
    // ---------- CoreProtocol instance accessors ----------
    /** @inheritdoc */
    get uri() {
        return PermissionsProtocol.uri;
    }
    /** @inheritdoc */
    get definition() {
        return PermissionsProtocol.definition;
    }
    // ---------- CoreProtocol lifecycle hooks ----------
    /** @inheritdoc */
    validateRecord(message, dataBytes) {
        PermissionsProtocol.validateSchema(message, dataBytes);
    }
    /**
     * Pre-processing hook for permission revocation records.
     * Validates that the revocation's `tags.protocol` matches the grant's scoped protocol.
     */
    async preProcessWrite(tenant, message, validationStateReader) {
        if (message.descriptor.protocolPath !== PermissionsProtocol.revocationPath) {
            return;
        }
        // fetch the parent grant to compare the scoped protocol against the revocation tag
        const permissionGrantId = message.descriptor.parentId;
        const grant = await validationStateReader.fetchGrant(tenant, permissionGrantId);
        const revokeTagProtocol = message.descriptor.tags?.protocol;
        const grantProtocol = 'protocol' in grant.scope ? grant.scope.protocol : undefined;
        if (grantProtocol !== revokeTagProtocol) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateRevocationProtocolTagMismatch, `Revocation protocol ${revokeTagProtocol} does not match grant protocol ${grantProtocol}`);
        }
    }
    /**
     * Post-processing hook for permission revocation records.
     * When a grant is revoked, all messages authorized by that grant and created
     * after the revocation timestamp are deleted from all stores.
     *
     * Deletion order is deliberate to avoid orphaned data in case of crash:
     *   1. data store  (large blobs first)
     *   2. message store
     */
    async postProcessWrite(tenant, recordsWrite, stores) {
        if (recordsWrite.message.descriptor.protocolPath !== PermissionsProtocol.revocationPath) {
            return;
        }
        const permissionGrantId = recordsWrite.message.descriptor.parentId;
        const grantAuthorizedMessagesQuery = {
            permissionGrantId,
            dateCreated: { gte: recordsWrite.message.descriptor.messageTimestamp },
        };
        const { messages: grantAuthorizedMessages } = await stores.messageStore.query(tenant, [grantAuthorizedMessagesQuery]);
        if (grantAuthorizedMessages.length === 0) {
            return;
        }
        // 1. Delete data from the data store first to avoid orphaned data blobs in case of crash.
        //    Only RecordsWrite messages with data larger than maxDataSizeAllowedToBeEncoded have data in the data store.
        for (const message of grantAuthorizedMessages) {
            if (message.descriptor.method === DwnMethodName.Write) {
                const recordsWriteMessage = message;
                if (recordsWriteMessage.descriptor.dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded) {
                    await stores.dataStore.delete(tenant, recordsWriteMessage.recordId, recordsWriteMessage.descriptor.dataCid);
                }
            }
        }
        const messageCids = await Promise.all(grantAuthorizedMessages.map((message) => Message.getCid(message)));
        // 2. Finally delete all messages from the message store.
        await Promise.all(messageCids.map((cid) => stores.messageStore.delete(tenant, cid)));
    }
    /** @inheritdoc */
    mapErrorToStatusCode(errorCode) {
        if (errorCode.startsWith('PermissionsProtocolValidate')) {
            return 400;
        }
        return undefined;
    }
    /**
     * Constructs an additional filter for protocol-scoped message queries so that
     * permission records (grants, requests, revocations) tagged with the target
     * protocol appear alongside that protocol's own records in sync/subscribe/read results.
     */
    constructAdditionalMessageFilter(filter) {
        const { protocol, messageTimestamp } = filter;
        if (protocol === undefined) {
            return undefined;
        }
        const taggedFilter = {
            protocol: PermissionsProtocol.uri,
            ...Records.convertTagsFilter({ protocol }),
        };
        if (messageTimestamp !== undefined) {
            const messageTimestampFilter = FilterUtility.convertRangeCriterion(messageTimestamp);
            if (messageTimestampFilter) {
                taggedFilter.messageTimestamp = messageTimestampFilter;
            }
        }
        return taggedFilter;
    }
    // ---------- Static utility methods ----------
    static parseRequest(base64UrlEncodedRequest) {
        return Encoder.base64UrlToObject(base64UrlEncodedRequest);
    }
    /**
     * Convenience method to create a permission request.
     */
    static async createRequest(options) {
        if (this.hasConflictingSubtreeScope(options.scope)) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateRequestScopeContextIdProtocolPathConflict, 'Permission request scopes cannot have both `contextId` and `protocolPath` present');
        }
        if (this.isRecordPermissionScope(options.scope) && options.scope.protocol === undefined) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateRequestRecordsScopeMissingProtocol, 'Permission request for Records must have a scope with a `protocol` property');
        }
        if (this.hasSubtreeScope(options.scope) && !this.hasProtocolScope(options.scope)) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateRequestSubtreeScopeMissingProtocol, 'Permission request subtree scopes must have a `protocol` property');
        }
        const scope = PermissionsProtocol.normalizePermissionScope(options.scope);
        const permissionRequestData = {
            description: options.description,
            delegated: options.delegated,
            scope,
            conditions: options.conditions,
        };
        // If the request is scoped to a protocol, the protocol tag must be included with the record.
        // This is done in order to ensure a subset message query filtered to a protocol includes the permission requests associated with it.
        let permissionTags = undefined;
        if (this.hasProtocolScope(scope)) {
            permissionTags = {
                protocol: scope.protocol
            };
        }
        const permissionRequestBytes = Encoder.objectToBytes(permissionRequestData);
        const recordsWrite = await RecordsWrite.create({
            signer: options.signer,
            messageTimestamp: options.dateRequested,
            protocol: PermissionsProtocol.uri,
            protocolPath: PermissionsProtocol.requestPath,
            dataFormat: 'application/json',
            data: permissionRequestBytes,
            tags: permissionTags,
        });
        const dataEncodedMessage = {
            ...recordsWrite.message,
            encodedData: Encoder.bytesToBase64Url(permissionRequestBytes)
        };
        return {
            recordsWrite,
            permissionRequestData,
            permissionRequestBytes,
            dataEncodedMessage
        };
    }
    /**
     * Convenience method to create a permission grant.
     */
    static async createGrant(options) {
        if (this.hasConflictingSubtreeScope(options.scope)) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateGrantScopeContextIdProtocolPathConflict, 'Permission grant scopes cannot have both `contextId` and `protocolPath` present');
        }
        if (this.isRecordPermissionScope(options.scope) && options.scope.protocol === undefined) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateGrantRecordsScopeMissingProtocol, 'Permission grants for Records must have a scope with a `protocol` property');
        }
        if (this.hasSubtreeScope(options.scope) && !this.hasProtocolScope(options.scope)) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolCreateGrantSubtreeScopeMissingProtocol, 'Permission grant subtree scopes must have a `protocol` property');
        }
        const scope = PermissionsProtocol.normalizePermissionScope(options.scope);
        const permissionGrantData = {
            dateExpires: options.dateExpires,
            requestId: options.requestId,
            description: options.description,
            delegated: options.delegated,
            scope,
            conditions: options.conditions,
            connectSession: options.connectSession,
        };
        // If the grant is scoped to a protocol, the protocol tag must be included with the record.
        // This is done in order to ensure a subset message query filtered to a protocol includes the permission grants associated with it.
        let permissionTags = undefined;
        if (this.hasProtocolScope(scope)) {
            permissionTags = {
                protocol: scope.protocol
            };
        }
        const permissionGrantBytes = Encoder.objectToBytes(permissionGrantData);
        const recordsWrite = await RecordsWrite.create({
            signer: options.signer,
            messageTimestamp: options.dateGranted,
            dateCreated: options.dateGranted,
            recipient: options.grantedTo,
            protocol: PermissionsProtocol.uri,
            protocolPath: PermissionsProtocol.grantPath,
            dataFormat: 'application/json',
            data: permissionGrantBytes,
            tags: permissionTags,
        });
        const dataEncodedMessage = {
            ...recordsWrite.message,
            encodedData: Encoder.bytesToBase64Url(permissionGrantBytes)
        };
        return {
            recordsWrite,
            permissionGrantData,
            permissionGrantBytes,
            dataEncodedMessage
        };
    }
    /**
     * Convenience method to create a permission revocation.
     */
    static async createRevocation(options) {
        const permissionRevocationData = {
            description: options.description,
        };
        const grantId = options.grant.id;
        // if the grant was scoped to a protocol, the protocol tag must be included in the revocation
        // This is done in order to ensure a subset message query filtered to a protocol includes the permission revocations associated with it.
        //
        // NOTE: the added tag is validated against the original grant when the revocation is processed by the DWN.
        let permissionTags = undefined;
        if (this.hasProtocolScope(options.grant.scope)) {
            const protocol = normalizeProtocolUrl(options.grant.scope.protocol);
            permissionTags = { protocol };
        }
        const permissionRevocationBytes = Encoder.objectToBytes(permissionRevocationData);
        const recordsWrite = await RecordsWrite.create({
            signer: options.signer,
            parentContextId: grantId, // NOTE: since the grant is the root record, its record ID is also the context ID
            protocol: PermissionsProtocol.uri,
            protocolPath: PermissionsProtocol.revocationPath,
            dataFormat: 'application/json',
            data: permissionRevocationBytes,
            tags: permissionTags,
        });
        const dataEncodedMessage = {
            ...recordsWrite.message,
            encodedData: Encoder.bytesToBase64Url(permissionRevocationBytes)
        };
        return {
            recordsWrite,
            permissionRevocationData,
            permissionRevocationBytes,
            dataEncodedMessage
        };
    }
    /**
     * Validates the given Permissions protocol RecordsWrite. It can be a request, grant, or revocation.
     */
    static validateSchema(recordsWriteMessage, dataBytes) {
        const dataString = Encoder.bytesToString(dataBytes);
        const dataObject = JSON.parse(dataString);
        if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.requestPath) {
            const permissionRequestData = dataObject;
            validateJsonSchema('PermissionRequestData', permissionRequestData);
            // more nuanced validation that are annoying/difficult to do using JSON schema
            PermissionsProtocol.validateScopeAndTags(permissionRequestData.scope, recordsWriteMessage);
        }
        else if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.grantPath) {
            validateJsonSchema('PermissionGrantData', dataObject);
            // more nuanced validation that are annoying/difficult to do using JSON schema
            const permissionGrantData = dataObject;
            PermissionsProtocol.validateScopeAndTags(permissionGrantData.scope, recordsWriteMessage);
            Time.validateTimestamp(permissionGrantData.dateExpires);
        }
        else if (recordsWriteMessage.descriptor.protocolPath === PermissionsProtocol.revocationPath) {
            validateJsonSchema('PermissionRevocationData', dataObject);
        }
        else {
            // defensive programming, should not be unreachable externally
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateSchemaUnexpectedRecord, `Unexpected permission record: ${recordsWriteMessage.descriptor.protocolPath}`);
        }
    }
    /**
     * Gets the scope from the given permission record.
     * If the record is a revocation, the scope is fetched from the grant that is being revoked.
     *
     * @param validationStateReader Used to fetch the grant for a revocation.
     */
    static async getScopeFromPermissionRecord(tenant, validationStateReader, incomingMessage) {
        if (incomingMessage.descriptor.protocol !== PermissionsProtocol.uri) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolGetScopeInvalidProtocol, `Unexpected protocol for permission record: ${incomingMessage.descriptor.protocol}`);
        }
        if (incomingMessage.descriptor.protocolPath === PermissionsProtocol.revocationPath) {
            const grant = await validationStateReader.fetchGrant(tenant, incomingMessage.descriptor.parentId);
            return grant.scope;
        }
        else if (incomingMessage.descriptor.protocolPath === PermissionsProtocol.grantPath) {
            const grant = PermissionGrant.parse(incomingMessage);
            return grant.scope;
        }
        else {
            // if the record is not a grant or revocation, it must be a request
            const request = PermissionRequest.parse(incomingMessage);
            return request.scope;
        }
    }
    /**
     * Normalizes the given permission scope if needed.
     * @returns The normalized permission scope.
     */
    static normalizePermissionScope(permissionScope) {
        const scope = { ...permissionScope };
        if (PermissionsProtocol.hasProtocolScope(scope)) {
            scope.protocol = normalizeProtocolUrl(scope.protocol);
        }
        return scope;
    }
    /**
     * Type guard to determine if the scope is a record permission scope.
     */
    static isRecordPermissionScope(scope) {
        return scope.interface === 'Records';
    }
    /**
     * Type guard to determine if the permission is a protocol-scoped
     */
    static hasProtocolScope(scope) {
        return 'protocol' in scope && scope.protocol !== undefined;
    }
    static hasSubtreeScope(scope) {
        return ('contextId' in scope && scope.contextId !== undefined)
            || ('protocolPath' in scope && scope.protocolPath !== undefined);
    }
    static hasConflictingSubtreeScope(scope) {
        return ('contextId' in scope && scope.contextId !== undefined)
            && ('protocolPath' in scope && scope.protocolPath !== undefined);
    }
    /**
     * Validates that tags must include a protocol tag that matches the scoped protocol.
     */
    static validateTags(requestOrGrant, scopedProtocol) {
        // the protocol tag must be included with the record.
        if (requestOrGrant.descriptor.tags?.protocol === undefined) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateScopeMissingProtocolTag, 'Permission grants must have a `tags` property that contains a protocol tag');
        }
        // The protocol tag must match the protocol in the scope
        const taggedProtocol = requestOrGrant.descriptor.tags.protocol;
        if (taggedProtocol !== scopedProtocol) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateScopeProtocolMismatch, `Permission grants must have a scope with a protocol that matches the tagged protocol: ${taggedProtocol}`);
        }
    }
    /**
     * Validates scope and tags of the given permission request or grant.
     */
    static validateScopeAndTags(scope, requestOrGrant) {
        // scoped protocol validations
        if (this.hasProtocolScope(scope)) {
            validateProtocolUrlNormalized(scope.protocol);
            this.validateTags(requestOrGrant, scope.protocol);
        }
        // `contextId` and `protocolPath` are mutually exclusive
        const hasContextId = 'contextId' in scope && scope.contextId !== undefined;
        const hasProtocolPath = 'protocolPath' in scope && scope.protocolPath !== undefined;
        if (hasContextId && hasProtocolPath) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateScopeContextIdProhibitedProperties, 'Permission grants cannot have both `contextId` and `protocolPath` present');
        }
        if ((hasContextId || hasProtocolPath) && !this.hasProtocolScope(scope)) {
            throw new DwnError(DwnErrorCode.PermissionsProtocolValidateScopeSubtreeScopeMissingProtocol, 'Permission grant subtree scopes must have a `protocol` property');
        }
    }
}
;
//# sourceMappingURL=permissions.js.map