import { checkActor } from './protocol-authorization-action.js';
import { DwnConstant } from './dwn-constant.js';
import { Encoder } from '../utils/encoder.js';
import { Encryption } from '../utils/encryption.js';
import { EncryptionControlDeliveryRecipientAuthority } from '../types/encryption-types.js';
import { GrantAuthorization } from './grant-authorization.js';
import { Jws } from '../utils/jws.js';
import { lexicographicalCompare } from '../utils/string.js';
import { Message } from './message.js';
import { PermissionConditionPublication } from '../types/permission-types.js';
import { PermissionGrant } from '../protocols/permission-grant.js';
import { PermissionScopeMatcher } from '../utils/permission-scope.js';
import { Records } from '../utils/records.js';
import { selectOccupantRecordIds } from '../utils/record-limit-occupancy.js';
import { SortDirection } from '../types/query-types.js';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
import { ENCRYPTION_CONTROL_AUDIENCE_PATH, ENCRYPTION_CONTROL_DELIVERY_PATH, isEncryptionControlPath } from './constants.js';
import { getRoleContextPrefix, getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
import { grantKeyScopeCoversDeliveredScope, isGrantKeyEligibleRecordsScope } from '../utils/grant-key-coverage.js';
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
export class EncryptionControl {
    static isControlMessage(message) {
        return isEncryptionControlPath(message.descriptor.protocolPath);
    }
    static isAudienceControlMessage(message) {
        return message.descriptor.protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH;
    }
    static isExactAudienceFilter(filter) {
        return EncryptionControl.getExactAudienceFilterTuple(filter) !== undefined;
    }
    static filterTargetsOnlyControlRecords(filter) {
        return typeof filter.protocolPath === 'string' && isEncryptionControlPath(filter.protocolPath);
    }
    static async authorizeControlReadRequest(input) {
        if (input.requester === undefined || input.requester === input.tenant) {
            return;
        }
        await EncryptionControl.getInvokedReadGrants({
            actorDid: input.requester,
            incomingMessage: input.incomingMessage,
            tenant: input.tenant,
            validationStateReader: input.validationStateReader,
        });
    }
    static buildAudienceRecordFilters(filters) {
        const audienceFilters = [];
        for (const filter of filters) {
            const protocolPath = filter.protocolPath;
            if (protocolPath === undefined) {
                audienceFilters.push({ ...filter, protocolPath: ENCRYPTION_CONTROL_AUDIENCE_PATH });
                continue;
            }
            if (typeof protocolPath === 'string') {
                if (protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH) {
                    audienceFilters.push(filter);
                }
                continue;
            }
            if (Array.isArray(protocolPath) && protocolPath.includes(ENCRYPTION_CONTROL_AUDIENCE_PATH)) {
                audienceFilters.push({ ...filter, protocolPath: ENCRYPTION_CONTROL_AUDIENCE_PATH });
            }
        }
        return audienceFilters;
    }
    /**
     * Resolves the current audience record for a role-audience tuple.
     *
     * Current-key projection orders stored audience records by:
     * tenant-authored actual signer first, then oldest `dateCreated`, then `recordId` ascending.
     */
    static async resolveCurrentAudienceRecord(input) {
        const records = await EncryptionControl.queryStoredAudienceRecordsForTuple(input);
        const currentRecordIds = selectOccupantRecordIds({
            records,
            max: 1,
            getScopeKey: (record) => EncryptionControl.getAudienceTupleKey(EncryptionControl.getRoleAudienceKeyId(record, 'audience')),
            compareRecords: (left, right) => EncryptionControl.compareAudienceProjectionCandidates(input.tenant, left, right),
        });
        const currentRecordId = currentRecordIds.values().next().value;
        return records.find((record) => record.recordId === currentRecordId);
    }
    static async projectCurrentAudienceRecords(input) {
        const currentRecordIds = new Set();
        const tupleKeys = new Set();
        for (const record of input.recordsWriteMessages) {
            if (!EncryptionControl.shouldProjectAudienceRecord(record, input.bypassFilters ?? [])) {
                continue;
            }
            const tags = EncryptionControl.getRoleAudienceKeyId(record, 'audience');
            tupleKeys.add(EncryptionControl.getAudienceTupleKey(tags));
        }
        for (const tupleKey of tupleKeys) {
            const tuple = EncryptionControl.parseAudienceTupleKey(tupleKey);
            const currentRecordId = await EncryptionControl.resolveCurrentAudienceRecordId({
                ...tuple,
                currentAudienceRecordIdCache: input.currentAudienceRecordIdCache,
                messageStore: input.messageStore,
                tenant: input.tenant,
            });
            if (currentRecordId !== undefined) {
                currentRecordIds.add(currentRecordId);
            }
        }
        return input.recordsWriteMessages.filter((record) => !EncryptionControl.shouldProjectAudienceRecord(record, input.bypassFilters ?? []) ||
            currentRecordIds.has(record.recordId));
    }
    static async projectCurrentAudienceRecordPage(input) {
        return {
            messages: await EncryptionControl.projectCurrentAudienceRecords({
                currentAudienceRecordIdCache: input.currentAudienceRecordIdCache,
                messageStore: input.messageStore,
                tenant: input.tenant,
                recordsWriteMessages: input.result.messages,
                bypassFilters: input.filters,
            }),
            cursor: input.result.cursor,
        };
    }
    static async filterVisibleControlRecords(input) {
        const visibleRecordsWrites = [];
        for (const recordsWrite of input.recordsWriteMessages) {
            const visible = await EncryptionControl.isVisibleControlRecord({
                incomingMessage: input.incomingMessage,
                recordsWriteMessage: recordsWrite,
                requester: input.requester,
                tenant: input.tenant,
                validationStateReader: input.validationStateReader,
            });
            if (visible) {
                visibleRecordsWrites.push(recordsWrite);
            }
        }
        return visibleRecordsWrites;
    }
    static async authorizeRead(input) {
        if (await EncryptionControl.canRead(input)) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionControlReadUnauthorized, 'requester is not authorized to read the encryption control record.');
    }
    static async canRead(input) {
        const { requester, recordsWriteMessage, tenant } = input;
        if (!EncryptionControl.isControlMessage(recordsWriteMessage)) {
            return true;
        }
        if (requester === tenant) {
            return true;
        }
        if (requester === undefined) {
            return false;
        }
        const recordType = EncryptionControl.getRecordType(recordsWriteMessage);
        if (recordType === 'delivery') {
            if (requester === recordsWriteMessage.descriptor.recipient ||
                requester === Message.getAuthor(recordsWriteMessage)) {
                return true;
            }
            return EncryptionControl.canReadDeliveryByDelegatedGrant({
                tenant,
                incomingMessage: input.incomingMessage,
                requester,
                recordsWriteMessage,
                tags: EncryptionControl.getDeliveryTags(recordsWriteMessage),
                validationStateReader: input.validationStateReader,
            });
        }
        const tags = EncryptionControl.getRoleAudienceKeyId(recordsWriteMessage, 'audience');
        if (EncryptionControl.exactAudienceRequestMatchesRecord(input.incomingMessage, tags, recordsWriteMessage.recordId)) {
            return true;
        }
        return EncryptionControl.canEnumerateAudience({
            tenant,
            actorDid: requester,
            incomingMessage: input.incomingMessage,
            tags,
            validationStateReader: input.validationStateReader,
        });
    }
    static async authorizeWrite(tenant, recordsWrite, validationStateReader) {
        const recordType = EncryptionControl.getRecordType(recordsWrite.message);
        const tags = EncryptionControl.getRoleAudienceKeyId(recordsWrite.message, recordType);
        const { protocolDefinition, ruleSet } = await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, recordsWrite.message, validationStateReader, recordType);
        const actor = await EncryptionControl.resolveEffectiveControlActor(tenant, recordsWrite, validationStateReader);
        await EncryptionControl.verifyActorCanCreateRole({
            tenant,
            actor,
            tags,
            protocolDefinition,
            ruleSet,
            validationStateReader,
            message: recordsWrite.message,
        });
    }
    static async validateReferentialIntegrity(tenant, recordsWrite, validationStateReader) {
        if (!await recordsWrite.isInitialWrite()) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'encryption control records are immutable.');
        }
        EncryptionControl.verifyInlineControlRecord(recordsWrite.message);
        const recordType = EncryptionControl.getRecordType(recordsWrite.message);
        if (recordType === 'audience') {
            const tags = EncryptionControl.getRoleAudienceKeyId(recordsWrite.message, 'audience');
            await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, recordsWrite.message, validationStateReader, 'audience');
            return;
        }
        const tags = EncryptionControl.getDeliveryTags(recordsWrite.message);
        await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, recordsWrite.message, validationStateReader, 'delivery');
        const audiences = await validationStateReader.queryAudienceRecords({
            tenant,
            protocol: tags.protocol,
            rolePath: tags.rolePath,
            contextId: tags.contextId,
            keyId: tags.keyId,
        });
        if (audiences.length === 0) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryAudienceMissing, 'delivery control record references a missing audience.');
        }
    }
    static async preProcessWrite(tenant, message, validationStateReader) {
        EncryptionControl.verifyInlineControlRecord(message);
        switch (message.descriptor.protocolPath) {
            case ENCRYPTION_CONTROL_AUDIENCE_PATH:
                await EncryptionControl.preProcessAudience(tenant, message, validationStateReader);
                return;
            case ENCRYPTION_CONTROL_DELIVERY_PATH:
                await EncryptionControl.preProcessDelivery(tenant, message, validationStateReader);
                return;
            default:
                throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, `unexpected encryption control path '${message.descriptor.protocolPath}'.`);
        }
    }
    static async validateRecord(input) {
        const { tenant, message, dataBytes, validationStateReader } = input;
        switch (message.descriptor.protocolPath) {
            case ENCRYPTION_CONTROL_AUDIENCE_PATH:
                await EncryptionControl.validateAudience(tenant, message, dataBytes, validationStateReader);
                return;
            case ENCRYPTION_CONTROL_DELIVERY_PATH:
                EncryptionControl.verifyEncryptedDelivery(message);
                return;
            default:
                throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, `unexpected encryption control path '${message.descriptor.protocolPath}'.`);
        }
    }
    static mapErrorToStatusCode(errorCode) {
        if (errorCode.startsWith('EncryptionControlValidate')) {
            return 400;
        }
        return undefined;
    }
    static async preProcessAudience(tenant, message, validationStateReader) {
        if (message.encryption !== undefined) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'audience control records must be plaintext.');
        }
        const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
        await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'audience');
    }
    static async preProcessDelivery(tenant, message, validationStateReader) {
        EncryptionControl.verifyEncryptedDelivery(message);
        if (message.descriptor.recipient === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryRecipientMissing, 'delivery control records must have a recipient.');
        }
        const tags = EncryptionControl.getDeliveryTags(message);
        await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'delivery');
        const audiences = await validationStateReader.queryAudienceRecords({
            tenant,
            protocol: tags.protocol,
            rolePath: tags.rolePath,
            contextId: tags.contextId,
            keyId: tags.keyId,
        });
        if (audiences.length === 0) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryAudienceMissing, 'delivery control record references a missing audience.');
        }
        await EncryptionControl.verifyDeliveryRecipientAuthority({
            tenant,
            message,
            tags,
            validationStateReader,
        });
    }
    static async validateAudience(tenant, message, dataBytes, validationStateReader) {
        if (message.encryption !== undefined) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'audience control records must be plaintext.');
        }
        const dataObject = JSON.parse(Encoder.bytesToString(dataBytes));
        validateJsonSchema('Audience', dataObject);
        const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
        EncryptionControl.verifyAudiencePayloadMatchesTags(dataObject, tags);
        const audienceKeyId = await Encryption.getKeyId(dataObject.publicKeyJwk);
        if (audienceKeyId !== dataObject.keyId) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceKeyIdMismatch, 'audience keyId must match publicKeyJwk thumbprint.');
        }
        const { ruleSet } = await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'audience');
        const sealingKeyId = await Encryption.getKeyId(ruleSet.$keyAgreement.publicKeyJwk);
        if (Object.is(sealingKeyId, dataObject.sealedPrivateKey.keyId)) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceSealKeyIdMismatch, 'audience seal keyId must match the role path $keyAgreement public key thumbprint.');
    }
    static verifyEncryptedDelivery(message) {
        if (message.encryption === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'delivery control records must be encrypted.');
        }
    }
    static verifyInlineControlRecord(message) {
        if (message.descriptor.published === true) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'encryption control records must not be published.');
        }
        if (message.descriptor.dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateUnexpectedRecord, 'encryption control records must be small enough for inline validation.');
        }
    }
    static verifyAudiencePayloadMatchesTags(payload, tags) {
        if (payload.protocol !== tags.protocol ||
            payload.rolePath !== tags.rolePath ||
            payload.contextId !== tags.contextId ||
            payload.keyId !== tags.keyId) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceTagsMismatch, 'audience tags must match payload fields.');
        }
    }
    static async verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, recordType) {
        if (tags.protocol !== message.descriptor.protocol) {
            const errorCode = recordType === 'audience'
                ? DwnErrorCode.EncryptionControlValidateAudienceTagsMismatch
                : DwnErrorCode.EncryptionControlValidateDeliveryTagsMismatch;
            throw new DwnError(errorCode, 'control record protocol tag must match descriptor protocol.');
        }
        return EncryptionControl.resolveRoleAudienceDefinition({
            tenant,
            tags,
            messageTimestamp: message.descriptor.messageTimestamp,
            validationStateReader,
        });
    }
    static verifyRoleAudienceContext(rolePath, contextId) {
        const parentDepth = rolePath.split('/').length - 1;
        if ((parentDepth === 0 && contextId !== '') || (parentDepth > 0 && contextId === '')) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceContextIdInvalid, 'role audience contextId is inconsistent with role path depth.');
        }
    }
    static async resolveEffectiveControlActor(tenant, recordsWrite, validationStateReader) {
        if (recordsWrite.isSignedByAuthorDelegate) {
            const grant = PermissionGrant.parse(recordsWrite.message.authorization.authorDelegatedGrant);
            await GrantAuthorization.performBaseValidation({
                incomingMessage: recordsWrite.message,
                expectedGrantor: recordsWrite.author,
                expectedGrantee: recordsWrite.signer,
                permissionGrant: grant,
                validationStateReader,
            });
            EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
            return { did: recordsWrite.signer, grant };
        }
        if (recordsWrite.isSignedByOwnerDelegate) {
            const grant = PermissionGrant.parse(recordsWrite.message.authorization.ownerDelegatedGrant);
            await GrantAuthorization.performBaseValidation({
                incomingMessage: recordsWrite.message,
                expectedGrantor: recordsWrite.owner,
                expectedGrantee: recordsWrite.ownerSignatureSigner,
                permissionGrant: grant,
                validationStateReader,
            });
            EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
            return { did: recordsWrite.ownerSignatureSigner, grant };
        }
        if (recordsWrite.owner !== undefined) {
            return { did: recordsWrite.owner };
        }
        const directGrantId = recordsWrite.signaturePayload === undefined
            ? undefined
            : Message.getPermissionGrantId(recordsWrite.signaturePayload);
        if (directGrantId === undefined) {
            return { did: recordsWrite.author };
        }
        const grant = await validationStateReader.fetchGrant(tenant, directGrantId);
        await GrantAuthorization.performBaseValidation({
            incomingMessage: recordsWrite.message,
            expectedGrantor: tenant,
            expectedGrantee: recordsWrite.author,
            permissionGrant: grant,
            validationStateReader,
        });
        EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
        return { did: recordsWrite.author, grant };
    }
    static async verifyActorCanCreateRole(input) {
        const { tenant, actor, tags, protocolDefinition, ruleSet, validationStateReader, message } = input;
        const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
        if (await EncryptionControl.actorCanCreateRole({
            tenant,
            actor,
            tags,
            protocolDefinition,
            ruleSet,
            validationStateReader,
            invokedRole: signaturePayload.protocolRole,
        })) {
            return;
        }
        throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceWriterUnauthorized, 'control records must be written by a DID authorized to create the referenced role.');
    }
    static async actorCanCreateRole(input) {
        const { tenant, actor, tags, protocolDefinition, ruleSet, validationStateReader, invokedRole } = input;
        if (actor.did === tenant) {
            return true;
        }
        if (actor.did === undefined) {
            return false;
        }
        if (actor.grant !== undefined && EncryptionControl.grantCoversRoleCreate(actor.grant, tags)) {
            return true;
        }
        const recordChain = await EncryptionControl.constructRoleParentChain(tenant, tags, validationStateReader);
        for (const actionRule of ruleSet.$actions ?? []) {
            if (!actionRule.can.includes(ProtocolAction.Create)) {
                continue;
            }
            if (actionRule.who === ProtocolActor.Anyone) {
                return true;
            }
            if (await EncryptionControl.roleRuleAllowsActor(tenant, actor.did, actionRule, tags, protocolDefinition, validationStateReader, invokedRole)) {
                return true;
            }
            if (await checkActor(actor.did, actionRule, recordChain, protocolDefinition)) {
                return true;
            }
        }
        return false;
    }
    static async verifyDeliveryRecipientAuthority(input) {
        const { tags } = input;
        if (tags.recipientAuthority !== EncryptionControlDeliveryRecipientAuthority.RoleHolder) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryRecipientAuthorityInvalid, `unsupported delivery recipient authority '${tags.recipientAuthority}'.`);
        }
        if (!await EncryptionControl.deliveryRecipientIsRoleHolder(input)) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryRecipientRoleRecordMissing, 'delivery recipient role record is missing.');
        }
    }
    static async deliveryRecipientIsRoleHolder(input) {
        return EncryptionControl.hasRoleRecord(input.tenant, input.message.descriptor.recipient, input.tags.protocol, input.tags.rolePath, input.tags.contextId, input.validationStateReader);
    }
    static grantCoversRoleCreate(grant, tags) {
        const scope = grant.scope;
        if (scope.interface !== DwnInterfaceName.Records || scope.method !== DwnMethodName.Write) {
            return false;
        }
        return PermissionScopeMatcher.matches(scope, {
            protocol: tags.protocol,
            protocolPath: tags.rolePath,
        });
    }
    static async canEnumerateAudience(input) {
        const { tenant, actorDid, incomingMessage, tags, validationStateReader } = input;
        const grants = await EncryptionControl.getInvokedReadGrants({
            tenant,
            actorDid,
            incomingMessage,
            validationStateReader,
        });
        if (grants.some(grant => EncryptionControl.grantCoversAudienceEnumeration(grant, tags))) {
            return true;
        }
        const { protocolDefinition, ruleSet } = await EncryptionControl.resolveRoleAudienceDefinition({
            tenant,
            tags,
            messageTimestamp: incomingMessage.descriptor.messageTimestamp,
            validationStateReader,
        });
        const invokedRole = incomingMessage.authorization === undefined
            ? undefined
            : Jws.decodePlainObjectPayload(incomingMessage.authorization.signature).protocolRole;
        return EncryptionControl.actorCanCreateRole({
            tenant,
            actor: { did: actorDid },
            tags,
            protocolDefinition,
            ruleSet,
            validationStateReader,
            invokedRole,
        });
    }
    static async canReadDeliveryByDelegatedGrant(input) {
        const { tenant, incomingMessage, requester, recordsWriteMessage, tags, validationStateReader } = input;
        const grants = await EncryptionControl.getInvokedReadGrants({
            tenant,
            actorDid: requester,
            incomingMessage,
            validationStateReader,
        });
        const recipient = recordsWriteMessage.descriptor.recipient;
        let protocolDefinition;
        for (const grant of grants) {
            if (recipient !== grant.grantor && recipient !== grant.grantee) {
                continue;
            }
            if (!isGrantKeyEligibleRecordsScope(grant.scope) || grant.scope.method !== DwnMethodName.Read) {
                continue;
            }
            const deliveredScope = {
                protocol: tags.protocol,
                protocolPath: tags.rolePath,
            };
            if (grantKeyScopeCoversDeliveredScope({
                grantScope: grant.scope,
                deliveredScope,
            })) {
                return true;
            }
            protocolDefinition ??= await validationStateReader.fetchProtocolDefinition(tenant, tags.protocol, incomingMessage.descriptor.messageTimestamp);
            if (grantKeyScopeCoversDeliveredScope({
                grantScope: grant.scope,
                deliveredScope,
                protocolDefinition,
            })) {
                return true;
            }
        }
        return false;
    }
    static async getInvokedReadGrants(input) {
        if (Message.isSignedByAuthorDelegate(input.incomingMessage)) {
            const grant = PermissionGrant.parse(input.incomingMessage.authorization.authorDelegatedGrant);
            await GrantAuthorization.performBaseValidation({
                incomingMessage: input.incomingMessage,
                expectedGrantor: grant.grantor,
                expectedGrantee: input.actorDid,
                permissionGrant: grant,
                validationStateReader: input.validationStateReader,
            });
            return [grant];
        }
        const { descriptor } = input.incomingMessage;
        if (!('permissionGrantId' in descriptor)) {
            return [];
        }
        const grantId = descriptor.permissionGrantId;
        if (grantId === undefined) {
            return [];
        }
        const grant = await input.validationStateReader.fetchGrant(input.tenant, grantId);
        await GrantAuthorization.performBaseValidation({
            incomingMessage: input.incomingMessage,
            expectedGrantor: input.tenant,
            expectedGrantee: input.actorDid,
            permissionGrant: grant,
            validationStateReader: input.validationStateReader,
        });
        return [grant];
    }
    static grantCoversAudienceEnumeration(grant, tags) {
        const scope = grant.scope;
        if (scope.interface !== DwnInterfaceName.Records || scope.method !== DwnMethodName.Read) {
            return false;
        }
        return PermissionScopeMatcher.matches(scope, {
            protocol: tags.protocol,
            protocolPath: tags.rolePath,
        });
    }
    static async resolveRoleAudienceDefinition(input) {
        const { tenant, tags, messageTimestamp, validationStateReader } = input;
        EncryptionControl.verifyRoleAudienceContext(tags.rolePath, tags.contextId);
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, tags.protocol, messageTimestamp);
        const ruleSet = getRuleSetAtPath(tags.rolePath, protocolDefinition.structure);
        if (ruleSet?.$role !== true || ruleSet.$keyAgreement === undefined) {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateAudienceRolePathInvalid, `role audience path '${tags.rolePath}' must be a role path with $keyAgreement.`);
        }
        return { protocolDefinition, ruleSet };
    }
    static verifyGrantConditions(message, grant) {
        if (grant.conditions?.publication === PermissionConditionPublication.Required && message.descriptor.published !== true) {
            throw new DwnError(DwnErrorCode.RecordsGrantAuthorizationConditionPublicationRequired, 'Permission grant requires message to be published');
        }
        if (grant.conditions?.publication === PermissionConditionPublication.Prohibited && message.descriptor.published === true) {
            throw new DwnError(DwnErrorCode.RecordsGrantAuthorizationConditionPublicationProhibited, 'Permission grant prohibits message from being published');
        }
    }
    static async constructRoleParentChain(tenant, tags, validationStateReader) {
        if (tags.contextId === '') {
            return [];
        }
        const parentRecordId = tags.contextId.split('/').pop();
        if (parentRecordId === undefined || parentRecordId === '') {
            return [];
        }
        return validationStateReader.constructRecordChain(tenant, parentRecordId);
    }
    static async roleRuleAllowsActor(tenant, actor, actionRule, tags, protocolDefinition, validationStateReader, invokedRole) {
        if (actionRule.role === undefined || invokedRole !== actionRule.role) {
            return false;
        }
        const resolved = EncryptionControl.resolveRoleReference(actionRule.role, tags.protocol, protocolDefinition);
        if (resolved === undefined) {
            return false;
        }
        return EncryptionControl.hasRoleRecord(tenant, actor, resolved.protocol, resolved.protocolPath, tags.contextId, validationStateReader);
    }
    static async hasRoleRecord(tenant, actor, protocol, rolePath, contextId, validationStateReader) {
        const contextIdPrefix = getRoleContextPrefix(rolePath, contextId);
        if (contextIdPrefix === undefined && rolePath.includes('/')) {
            return false;
        }
        return validationStateReader.hasMatchingRoleRecord({
            tenant,
            protocol,
            protocolPath: rolePath,
            recipient: actor,
            contextIdPrefix,
        });
    }
    static resolveRoleReference(roleRef, currentProtocol, protocolDefinition) {
        if (!isCrossProtocolRef(roleRef)) {
            return { protocol: currentProtocol, protocolPath: roleRef };
        }
        const parsed = parseCrossProtocolRef(roleRef);
        if (parsed === undefined) {
            return undefined;
        }
        const protocol = protocolDefinition.uses?.[parsed.alias];
        if (protocol === undefined) {
            return undefined;
        }
        return { protocol, protocolPath: parsed.protocolPath };
    }
    static getRoleAudienceKeyId(message, recordType) {
        return {
            protocol: EncryptionControl.getRequiredStringTag(message, 'protocol', recordType),
            rolePath: EncryptionControl.getRequiredStringTag(message, 'rolePath', recordType),
            contextId: EncryptionControl.getRequiredStringTag(message, 'contextId', recordType),
            keyId: EncryptionControl.getRequiredStringTag(message, 'keyId', recordType),
        };
    }
    static getDeliveryTags(message) {
        const tags = EncryptionControl.getRoleAudienceKeyId(message, 'delivery');
        return {
            ...tags,
            recipientAuthority: EncryptionControl.getRequiredStringTag(message, 'recipientAuthority', 'delivery'),
        };
    }
    static getRecordType(message) {
        return message.descriptor.protocolPath === ENCRYPTION_CONTROL_DELIVERY_PATH ? 'delivery' : 'audience';
    }
    static async isVisibleControlRecord(input) {
        if (!EncryptionControl.isControlMessage(input.recordsWriteMessage)) {
            return true;
        }
        try {
            return await EncryptionControl.canRead({
                tenant: input.tenant,
                incomingMessage: input.incomingMessage,
                requester: input.requester,
                recordsWriteMessage: input.recordsWriteMessage,
                validationStateReader: input.validationStateReader,
            });
        }
        catch (error) {
            if (error instanceof DwnError) {
                return false;
            }
            throw error;
        }
    }
    static exactAudienceFilterMatchesRecord(filter, tags, recordId) {
        if (filter.recordId === recordId) {
            return true;
        }
        const tuple = EncryptionControl.getExactAudienceFilterTuple(filter);
        if (tuple === undefined) {
            return false;
        }
        return tuple.protocol === tags.protocol &&
            tuple.rolePath === tags.rolePath &&
            tuple.contextId === tags.contextId &&
            (tuple.keyId === undefined || tuple.keyId === tags.keyId);
    }
    static getExactAudienceFilterTuple(filter) {
        if (filter.protocol === undefined || filter.protocolPath !== ENCRYPTION_CONTROL_AUDIENCE_PATH) {
            return undefined;
        }
        const protocol = EncryptionControl.getExactFilterTag(filter, 'protocol');
        const rolePath = EncryptionControl.getExactFilterTag(filter, 'rolePath');
        const contextId = EncryptionControl.getExactFilterTag(filter, 'contextId');
        if (protocol !== filter.protocol || rolePath === undefined || contextId === undefined) {
            return undefined;
        }
        const keyId = EncryptionControl.getExactFilterTag(filter, 'keyId');
        if (keyId === undefined) {
            return { protocol, rolePath, contextId };
        }
        return { protocol, rolePath, contextId, keyId };
    }
    static exactAudienceRequestMatchesRecord(incomingMessage, tags, recordId) {
        const { descriptor } = incomingMessage;
        if ('filter' in descriptor) {
            return EncryptionControl.exactAudienceFilterMatchesRecord(descriptor.filter, tags, recordId);
        }
        return false;
    }
    static getExactFilterTag(filter, tag) {
        const value = filter.tags?.[tag];
        return typeof value === 'string' ? value : undefined;
    }
    static async queryStoredAudienceRecordsForTuple(input) {
        const filter = {
            interface: DwnInterfaceName.Records,
            method: DwnMethodName.Write,
            isLatestBaseState: true,
            protocol: input.protocol,
            protocolPath: ENCRYPTION_CONTROL_AUDIENCE_PATH,
            'tag.protocol': input.protocol,
            'tag.rolePath': input.rolePath,
            'tag.contextId': input.contextId,
        };
        const { messages } = await input.messageStore.query(input.tenant, [filter], { dateCreated: SortDirection.Ascending });
        return messages.filter(Records.isRecordsWrite);
    }
    static async resolveCurrentAudienceRecordId(input) {
        const tupleKey = EncryptionControl.getAudienceTupleKey(input);
        if (input.currentAudienceRecordIdCache?.has(tupleKey) === true) {
            return input.currentAudienceRecordIdCache.get(tupleKey);
        }
        const current = await EncryptionControl.resolveCurrentAudienceRecord(input);
        const currentRecordId = current?.recordId;
        input.currentAudienceRecordIdCache?.set(tupleKey, currentRecordId);
        return currentRecordId;
    }
    static shouldProjectAudienceRecord(message, bypassFilters) {
        return EncryptionControl.isAudienceControlMessage(message) &&
            !bypassFilters.some((filter) => EncryptionControl.audienceProjectionBypassFilterMatchesRecord(filter, message));
    }
    static audienceProjectionBypassFilterMatchesRecord(filter, message) {
        if (filter.recordId === message.recordId) {
            return true;
        }
        const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
        return filter.protocol === tags.protocol &&
            filter.protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH &&
            filter['tag.protocol'] === tags.protocol &&
            filter['tag.rolePath'] === tags.rolePath &&
            filter['tag.contextId'] === tags.contextId &&
            filter['tag.keyId'] === tags.keyId;
    }
    static getAudienceTupleKey(input) {
        return JSON.stringify([input.protocol, input.rolePath, input.contextId]);
    }
    static parseAudienceTupleKey(key) {
        const [protocol, rolePath, contextId] = JSON.parse(key);
        return { protocol, rolePath, contextId };
    }
    static compareAudienceProjectionCandidates(tenant, left, right) {
        const leftIsTenantSigned = Message.getSigner(left) === tenant;
        const rightIsTenantSigned = Message.getSigner(right) === tenant;
        if (leftIsTenantSigned !== rightIsTenantSigned) {
            return leftIsTenantSigned ? -1 : 1;
        }
        const dateComparison = lexicographicalCompare(left.descriptor.dateCreated, right.descriptor.dateCreated);
        if (dateComparison !== 0) {
            return dateComparison;
        }
        return lexicographicalCompare(left.recordId, right.recordId);
    }
    static getRequiredStringTag(message, tag, recordType) {
        const value = message.descriptor.tags?.[tag];
        if (typeof value !== 'string') {
            const errorCode = recordType === 'audience'
                ? DwnErrorCode.EncryptionControlValidateAudienceMissingRequiredTag
                : DwnErrorCode.EncryptionControlValidateDeliveryMissingRequiredTag;
            throw new DwnError(errorCode, `${recordType} control records must include string tag '${tag}'.`);
        }
        return value;
    }
    static getOptionalStringTag(tags, tag) {
        const value = tags?.[tag];
        if (value === undefined) {
            return undefined;
        }
        if (typeof value !== 'string') {
            throw new DwnError(DwnErrorCode.EncryptionControlValidateDeliveryMissingRequiredTag, `delivery control tag '${tag}' must be a string.`);
        }
        return value;
    }
}
//# sourceMappingURL=encryption-control.js.map