import { getRuleSetAtPath } from '../utils/protocols.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
import { authorizeAgainstAllowedActions, verifyInvokedRole } from './protocol-authorization-action.js';
import { verifyAsRoleRecordIfNeeded, verifyImmutability, verifyProtocolPathAndContextId, verifyRecordLimit, verifySizeLimit, verifySquashEligibility, verifyTagsIfNeeded, verifyTypeWithComposition, } from './protocol-authorization-validation.js';
export class ProtocolAuthorization {
    /**
     * Performs validation on the structure of RecordsWrite messages that use a protocol.
     * @throws {Error} if validation fails.
     */
    static async validateReferentialIntegrity(tenant, incomingMessage, validationStateReader) {
        const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
        // fetch the protocol definition active at the incoming message timestamp
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, incomingMessage.message.descriptor.protocol, protocolDefinitionTimestamp);
        // verify declared protocol type exists in protocol and that it conforms to type specification.
        // For cross-protocol composition, the type may be defined in a referenced protocol.
        await verifyTypeWithComposition(tenant, incomingMessage.message, protocolDefinition, validationStateReader, protocolDefinitionTimestamp);
        // validate `protocolPath`
        await verifyProtocolPathAndContextId(tenant, incomingMessage, validationStateReader, protocolDefinitionTimestamp);
        // get the rule set for the inbound message
        const ruleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message.descriptor.protocolPath, protocolDefinition);
        // Validate as a role record if the incoming message is writing a role record
        await verifyAsRoleRecordIfNeeded(tenant, incomingMessage, ruleSet, validationStateReader);
        // Verify size limit
        verifySizeLimit(incomingMessage, ruleSet);
        // Verify protocol tags
        verifyTagsIfNeeded(incomingMessage, ruleSet);
        // Verify immutability — reject updates to write-once records
        await verifyImmutability(incomingMessage, ruleSet);
        // Verify squash eligibility — ensure squash writes are at $squash: true paths and are initial writes
        await verifySquashEligibility(incomingMessage, ruleSet);
        // Verify record count limit
        await verifyRecordLimit(incomingMessage, ruleSet);
        return ruleSet;
    }
    /**
     * Revalidates a stored initial write against the protocol definition that governed its creation timestamp.
     *
     * This is used only for destructive config-history repair, so it deliberately validates config-owned
     * structure and avoids live dependency checks. Missing grant, role, or parent records must not cause
     * an already-admitted record to be hard-purged.
     */
    static async validateStoredInitialWrite(tenant, incomingMessage, validationStateReader) {
        await ProtocolAuthorization.verifyStoredInitialWrite(incomingMessage);
        const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, incomingMessage.message.descriptor.protocol, protocolDefinitionTimestamp);
        await verifyTypeWithComposition(tenant, incomingMessage.message, protocolDefinition, validationStateReader, protocolDefinitionTimestamp);
        const ruleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message.descriptor.protocolPath, protocolDefinition);
        ProtocolAuthorization.verifyStoredInitialWriteRoleRecipientIfNeeded(incomingMessage, ruleSet);
        verifySizeLimit(incomingMessage, ruleSet);
        verifyTagsIfNeeded(incomingMessage, ruleSet);
        await verifySquashEligibility(incomingMessage, ruleSet);
        ProtocolAuthorization.verifyStoredInitialWriteCreateAction(tenant, incomingMessage, ruleSet);
        // `verifyRecordLimit()` is intentionally not replayed here. It only checks strategy
        // support for new record candidates; read-time occupancy projection decides visibility.
    }
    /**
     * Performs protocol-based authorization against the incoming RecordsWrite message.
     * @throws {Error} if authorization fails.
     */
    static async authorizeWrite(tenant, incomingMessage, validationStateReader) {
        const existingInitialWrite = await validationStateReader.fetchInitialWrite(tenant, incomingMessage.message.recordId);
        let recordChain;
        if (existingInitialWrite === undefined) {
            // NOTE: we can assume this message is an initial write because an existing initial write does not exist.
            // Additionally, we check further down in the `RecordsWriteHandler` if the incoming message is an initialWrite,
            // so we don't check explicitly here to avoid an unnecessary duplicate check.
            recordChain = await validationStateReader.constructRecordChain(tenant, incomingMessage.message.descriptor.parentId);
        }
        else {
            recordChain = await validationStateReader.constructRecordChain(tenant, incomingMessage.message.recordId);
        }
        const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
        // fetch the protocol definition active at the incoming message timestamp
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, incomingMessage.message.descriptor.protocol, protocolDefinitionTimestamp);
        // get the rule set for the inbound message
        const ruleSet = ProtocolAuthorization.getRuleSet(incomingMessage.message.descriptor.protocolPath, protocolDefinition);
        // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
        await verifyInvokedRole(tenant, incomingMessage, incomingMessage.message.descriptor.protocol, incomingMessage.message.contextId, protocolDefinition, validationStateReader, protocolDefinitionTimestamp);
        // verify method invoked against the allowed actions in the rule set
        await authorizeAgainstAllowedActions(tenant, incomingMessage, ruleSet, recordChain, validationStateReader, protocolDefinition);
    }
    /**
     * Performs protocol-based authorization against the incoming `RecordsRead` message.
     * @param newestRecordsWrite The latest RecordsWrite associated with the recordId being read.
     * @throws {Error} if authorization fails.
     */
    static async authorizeRead(tenant, incomingMessage, newestRecordsWrite, validationStateReader) {
        // fetch record chain
        const recordChain = await validationStateReader.constructRecordChain(tenant, newestRecordsWrite.message.recordId);
        const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
        // fetch the protocol definition active at the incoming message timestamp
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, newestRecordsWrite.message.descriptor.protocol, protocolDefinitionTimestamp);
        // get the rule set for the inbound message
        const ruleSet = ProtocolAuthorization.getRuleSet(newestRecordsWrite.message.descriptor.protocolPath, protocolDefinition);
        // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
        await verifyInvokedRole(tenant, incomingMessage, newestRecordsWrite.message.descriptor.protocol, newestRecordsWrite.message.contextId, protocolDefinition, validationStateReader, protocolDefinitionTimestamp);
        // verify method invoked against the allowed actions in the rule set
        await authorizeAgainstAllowedActions(tenant, incomingMessage, ruleSet, recordChain, validationStateReader, protocolDefinition);
    }
    static async authorizeQueryOrSubscribe(tenant, incomingMessage, validationStateReader) {
        const { protocol, protocolPath, contextId } = incomingMessage.message.descriptor.filter;
        // fetch the protocol definition
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, protocol, // `authorizeQueryOrSubscribe` is only called if `protocol` is present
        incomingMessage.message.descriptor.messageTimestamp);
        // get the rule set for the inbound message
        const ruleSet = ProtocolAuthorization.getRuleSet(protocolPath, // presence of `protocolPath` is verified in `parse()`
        protocolDefinition);
        // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
        await verifyInvokedRole(tenant, incomingMessage, protocol, contextId, protocolDefinition, validationStateReader);
        // verify method invoked against the allowed actions in the rule set
        await authorizeAgainstAllowedActions(tenant, incomingMessage, ruleSet, [], // record chain is not relevant to queries or subscriptions
        validationStateReader, protocolDefinition);
    }
    /**
     * Performs protocol-based authorization against the incoming `RecordsDelete` message.
     * @param recordsWrite A `RecordsWrite` of the record being deleted.
     */
    static async authorizeDelete(tenant, incomingMessage, recordsWrite, validationStateReader) {
        // fetch record chain
        const recordChain = await validationStateReader.constructRecordChain(tenant, incomingMessage.message.descriptor.recordId);
        const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
        // fetch the protocol definition active at the incoming message timestamp
        const protocolDefinition = await validationStateReader.fetchProtocolDefinition(tenant, recordsWrite.message.descriptor.protocol, protocolDefinitionTimestamp);
        // get the rule set for the inbound message
        const ruleSet = ProtocolAuthorization.getRuleSet(recordsWrite.message.descriptor.protocolPath, protocolDefinition);
        // If the incoming message has `protocolRole` in the descriptor, validate the invoked role
        await verifyInvokedRole(tenant, incomingMessage, recordsWrite.message.descriptor.protocol, recordsWrite.message.contextId, protocolDefinition, validationStateReader, protocolDefinitionTimestamp);
        // verify method invoked against the allowed actions in the rule set
        await authorizeAgainstAllowedActions(tenant, incomingMessage, ruleSet, recordChain, validationStateReader, protocolDefinition);
    }
    /**
     * Gets the rule set corresponding to the given protocolPath.
     */
    static getRuleSet(protocolPath, protocolDefinition) {
        const ruleSet = getRuleSetAtPath(protocolPath, protocolDefinition.structure);
        if (ruleSet === undefined) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationMissingRuleSet, `No rule set defined for protocolPath ${protocolPath}`);
        }
        return ruleSet;
    }
    static async verifyStoredInitialWrite(incomingMessage) {
        if (await incomingMessage.isInitialWrite()) {
            return;
        }
        throw new DwnError(DwnErrorCode.ProtocolAuthorizationInitialWriteRevalidationNotInitial, 'stored write revalidation only supports initial RecordsWrite messages');
    }
    static verifyStoredInitialWriteRoleRecipientIfNeeded(incomingMessage, ruleSet) {
        if (ruleSet.$role !== true || incomingMessage.message.descriptor.recipient !== undefined) {
            return;
        }
        throw new DwnError(DwnErrorCode.ProtocolAuthorizationStoredInitialWriteRoleMissingRecipient, 'role records must have a recipient');
    }
    static verifyStoredInitialWriteCreateAction(tenant, incomingMessage, ruleSet) {
        if (ProtocolAuthorization.isStoredInitialWriteDirectlyAuthorized(tenant, incomingMessage)) {
            return;
        }
        const actions = incomingMessage.message.descriptor.squash === true
            ? [ProtocolAction.Squash, ProtocolAction.Create]
            : [ProtocolAction.Create];
        const actionRules = ruleSet.$actions;
        if (actionRules === undefined) {
            throw new DwnError(DwnErrorCode.ProtocolAuthorizationStoredInitialWriteActionRulesNotFound, `no create action rule defined for stored RecordsWrite by author ${incomingMessage.author}`);
        }
        const invokedRole = incomingMessage.signaturePayload?.protocolRole;
        for (const actionRule of actionRules) {
            if (!actionRule.can.some((allowedAction) => actions.includes(allowedAction))) {
                continue;
            }
            if (invokedRole !== undefined) {
                if (actionRule.role === invokedRole) {
                    return;
                }
                continue;
            }
            if (actionRule.who === ProtocolActor.Anyone) {
                return;
            }
            // Author/recipient-of rules depend on the parent chain. This repair path preserves
            // instead of hard-purging when validity depends on mutable or missing dependency records.
            if (actionRule.who === ProtocolActor.Author || actionRule.who === ProtocolActor.Recipient) {
                return;
            }
        }
        throw new DwnError(DwnErrorCode.ProtocolAuthorizationStoredInitialWriteActionNotAllowed, `stored RecordsWrite by author ${incomingMessage.author} is not allowed by the resolved protocol config`);
    }
    static isStoredInitialWriteDirectlyAuthorized(tenant, incomingMessage) {
        return incomingMessage.owner !== undefined ||
            incomingMessage.author === tenant ||
            incomingMessage.isSignedByAuthorDelegate ||
            incomingMessage.isSignedByOwnerDelegate ||
            incomingMessage.signaturePayload?.permissionGrantId !== undefined;
    }
}
//# sourceMappingURL=protocol-authorization.js.map