import { Cid } from '../utils/cid.js';
import { Encoder } from '../utils/encoder.js';
import { GeneralJwsBuilder } from '../jose/jws/general/builder.js';
import { Jws } from '../utils/jws.js';
import { lexicographicalCompare } from '../utils/string.js';
import { removeUndefinedProperties } from '@enbox/common';
import { validateJsonSchema } from '../schema-validator.js';
import { DwnError, DwnErrorCode } from './dwn-error.js';
/**
 * A class containing utility methods for working with DWN messages.
 */
export class Message {
    /**
     * Gets the DID of the author of the given message.
     */
    static getAuthor(message) {
        if (message.authorization === undefined) {
            return undefined;
        }
        let author;
        if (message.authorization.authorDelegatedGrant === undefined) {
            author = Message.getSigner(message);
        }
        else {
            author = Message.getSigner(message.authorization.authorDelegatedGrant);
        }
        return author;
    }
    /**
     * Validates the given message against the corresponding JSON schema.
     * @throws {Error} if fails validation.
     */
    static validateJsonSchema(rawMessage) {
        const dwnInterface = rawMessage.descriptor.interface;
        const dwnMethod = rawMessage.descriptor.method;
        const schemaLookupKey = dwnInterface + dwnMethod;
        // throws an error if message is invalid
        validateJsonSchema(schemaLookupKey, rawMessage);
    }
    ;
    /**
     * Gets the DID of the signer of the given message, returns `undefined` if message is not signed.
     */
    static getSigner(message) {
        if (message.authorization === undefined) {
            return undefined;
        }
        const signer = Jws.getSignerDid(message.authorization.signature.signatures[0]);
        return signer;
    }
    /**
     * Gets the CID of the given message.
     */
    static async getCid(message) {
        // NOTE: we wrap the `computeCid()` here in case that
        // the message will contain properties that should not be part of the CID computation
        // and we need to strip them out (like `encodedData` that we historically had for a long time),
        // but we can remove this method entirely if the code becomes stable and it is apparent that the wrapper is not needed
        // ^--- seems like we might need to keep this around for now.
        const rawMessage = { ...message };
        if (rawMessage.encodedData) {
            delete rawMessage.encodedData;
        }
        const cid = await Cid.computeCid(rawMessage);
        return cid;
    }
    /**
     * Compares message CID in lexicographical order according to the spec.
     * @returns 1 if `a` is larger than `b`; -1 if `a` is smaller/older than `b`; 0 otherwise (same message)
     */
    static async compareCid(a, b) {
        // the < and > operators compare strings in lexicographical order
        const cidA = await Message.getCid(a);
        const cidB = await Message.getCid(b);
        return lexicographicalCompare(cidA, cidB);
    }
    /**
     * Creates the `authorization` property to be included in a DWN message.
     * @param signer Message signer.
     * @returns {AuthorizationModel} used as an `authorization` property.
     */
    static async createAuthorization(input) {
        const { descriptor, signer, delegatedGrant, permissionGrantId, permissionGrantIds, protocolRole } = input;
        let delegatedGrantId;
        if (delegatedGrant !== undefined) {
            delegatedGrantId = await Message.getCid(delegatedGrant);
        }
        const signature = await Message.createSignature(descriptor, signer, { delegatedGrantId, permissionGrantId, permissionGrantIds, protocolRole });
        const authorization = {
            signature
        };
        if (delegatedGrant !== undefined) {
            authorization.authorDelegatedGrant = delegatedGrant;
        }
        return authorization;
    }
    /**
     * Creates a generic signature from the given DWN message descriptor by including `descriptorCid` as the required property in the signature payload.
     * NOTE: there is an opportunity to consolidate RecordsWrite.createSignerSignature() wth this method
     */
    static async createSignature(descriptor, signer, additionalPayloadProperties) {
        const descriptorCid = await Cid.computeCid(descriptor);
        const { delegatedGrantId, permissionGrantId, permissionGrantIds, protocolRole } = additionalPayloadProperties ?? {};
        const permissionGrantInvocation = Message.normalizePermissionGrantInvocation({ permissionGrantId, permissionGrantIds });
        const signaturePayload = {
            descriptorCid,
            delegatedGrantId,
            protocolRole,
            ...permissionGrantInvocation
        };
        removeUndefinedProperties(signaturePayload);
        const signaturePayloadBytes = Encoder.objectToBytes(signaturePayload);
        const builder = await GeneralJwsBuilder.create(signaturePayloadBytes, [signer]);
        const signature = builder.getJws();
        return signature;
    }
    /**
     * Normalizes permission grant invocation before it is written into a descriptor
     * and signed payload.
     *
     * Direct operation messages use `permissionGrantId`; Messages operations use
     * canonicalized `permissionGrantIds` so a request can invoke a grant set.
     */
    static normalizePermissionGrantInvocation(input) {
        if (input.permissionGrantId !== undefined && input.permissionGrantIds !== undefined) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantCreateInvocationAmbiguous, 'permission grant invocation must use either `permissionGrantId` or `permissionGrantIds`, not both');
        }
        if (input.permissionGrantId !== undefined) {
            return { permissionGrantId: input.permissionGrantId };
        }
        if (input.permissionGrantIds === undefined) {
            return {};
        }
        const permissionGrantIds = Message.normalizePermissionGrantIds(input);
        return { permissionGrantIds };
    }
    /**
     * Returns the permission grant ID carried by a direct operation signature payload.
     */
    static getPermissionGrantId(signaturePayload) {
        return signaturePayload.permissionGrantId;
    }
    /**
     * Returns all permission grant IDs invoked by a signature payload.
     *
     * Direct Records/Protocols operations carry a single `permissionGrantId`.
     * Messages operations carry `permissionGrantIds`. Generic cleanup and
     * revocation code uses this helper across both wire shapes.
     */
    static getPermissionGrantIds(signaturePayload) {
        if (signaturePayload.permissionGrantId !== undefined) {
            return [signaturePayload.permissionGrantId];
        }
        return signaturePayload.permissionGrantIds ?? [];
    }
    /**
     * @returns newest message in the array. `undefined` if given array is empty.
     */
    static async getNewestMessage(messages) {
        let currentNewestMessage = undefined;
        for (const message of messages) {
            if (currentNewestMessage === undefined || await Message.isNewer(message, currentNewestMessage)) {
                currentNewestMessage = message;
            }
        }
        return currentNewestMessage;
    }
    /**
     * @returns oldest message in the array. `undefined` if given array is empty.
     */
    static async getOldestMessage(messages) {
        let currentOldestMessage = undefined;
        for (const message of messages) {
            if (currentOldestMessage === undefined || await Message.isOlder(message, currentOldestMessage)) {
                currentOldestMessage = message;
            }
        }
        return currentOldestMessage;
    }
    /**
     * Checks if first message is newer than second message.
     * @returns `true` if `a` is newer than `b`; `false` otherwise
     */
    static async isNewer(a, b) {
        const aIsNewer = (await Message.compareMessageTimestamp(a, b) > 0);
        return aIsNewer;
    }
    /**
     * Checks if first message is older than second message.
     * @returns `true` if `a` is older than `b`; `false` otherwise
     */
    static async isOlder(a, b) {
        const aIsOlder = (await Message.compareMessageTimestamp(a, b) < 0);
        return aIsOlder;
    }
    /**
     * See if the given message is signed by an author-delegate.
     */
    static isSignedByAuthorDelegate(message) {
        return message.authorization?.authorDelegatedGrant !== undefined;
    }
    /**
     * See if the given message is signed by an owner-delegate.
     */
    static isSignedByOwnerDelegate(message) {
        return message.authorization?.ownerDelegatedGrant !== undefined;
    }
    /**
     * Compares the `messageTimestamp` of the given messages with a fallback to message CID according to the spec.
     * @returns 1 if `a` is larger/newer than `b`; -1 if `a` is smaller/older than `b`; 0 otherwise (same age)
     */
    static async compareMessageTimestamp(a, b) {
        if (a.descriptor.messageTimestamp > b.descriptor.messageTimestamp) {
            return 1;
        }
        else if (a.descriptor.messageTimestamp < b.descriptor.messageTimestamp) {
            return -1;
        }
        // else `messageTimestamp` is the same between a and b
        // compare the `dataCid` instead, the < and > operators compare strings in lexicographical order
        return Message.compareCid(a, b);
    }
    /**
     * Validates the structural integrity of the message signature given:
     * 1. The message signature must contain exactly 1 signature
     * 2. Passes JSON schema validation
     * 3. The `descriptorCid` property matches the CID of the message descriptor
     * NOTE: signature is NOT verified.
     * @param payloadJsonSchemaKey The key to look up the JSON schema referenced in `compile-validators.js` and perform payload schema validation on.
     * @param options Additional structural validation controls.
     * @returns the parsed JSON payload object if validation succeeds.
     */
    static async validateSignatureStructure(messageSignature, messageDescriptor, payloadJsonSchemaKey = 'GenericSignaturePayload', options = {}) {
        if (messageSignature.signatures.length !== 1) {
            throw new DwnError(DwnErrorCode.AuthenticationMoreThanOneSignatureNotSupported, 'expected no more than 1 signature for authorization purpose');
        }
        // validate payload integrity
        const payloadJson = Jws.decodePlainObjectPayload(messageSignature);
        validateJsonSchema(payloadJsonSchemaKey, payloadJson);
        // `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message
        const { descriptorCid } = payloadJson;
        const expectedDescriptorCid = await Cid.computeCid(messageDescriptor);
        if (descriptorCid !== expectedDescriptorCid) {
            throw new DwnError(DwnErrorCode.AuthenticateDescriptorCidMismatch, `provided descriptorCid ${descriptorCid} does not match expected CID ${expectedDescriptorCid}`);
        }
        if (options.validatePermissionGrantInvocation !== false) {
            Message.validatePermissionGrantInvocationMatchesPayload(messageDescriptor, payloadJson);
        }
        return payloadJson;
    }
    static normalizePermissionGrantIds(input) {
        if (input.permissionGrantIds === undefined) {
            return [];
        }
        if (input.permissionGrantIds.length === 0) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantIdsEmpty, '`permissionGrantIds` must contain at least one grant ID');
        }
        return [...new Set(input.permissionGrantIds)].sort(lexicographicalCompare);
    }
    static validatePermissionGrantInvocationCanonical(input) {
        if (input.permissionGrantId !== undefined && input.permissionGrantIds !== undefined) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantValidateInvocationAmbiguous, 'permission grant invocation must use either `permissionGrantId` or `permissionGrantIds`, not both');
        }
        if (input.permissionGrantId !== undefined) {
            return [input.permissionGrantId];
        }
        const normalizedPermissionGrantIds = Message.normalizePermissionGrantIds(input);
        if (input.permissionGrantIds !== undefined && !Message.areStringArraysEqual(input.permissionGrantIds, normalizedPermissionGrantIds)) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantIdsNotCanonical, '`permissionGrantIds` must be sorted and deduplicated');
        }
        return normalizedPermissionGrantIds;
    }
    static validatePermissionGrantInvocationMatchesPayload(descriptor, signaturePayload) {
        if (descriptor.permissionGrantId !== signaturePayload.permissionGrantId) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantDescriptorPayloadMismatch, 'permission grant invocation in descriptor does not match signature payload');
        }
        const descriptorPermissionGrantIds = Message.validatePermissionGrantInvocationCanonical(descriptor);
        const payloadPermissionGrantIds = Message.validatePermissionGrantInvocationCanonical(signaturePayload);
        if (!Message.areStringArraysEqual(descriptorPermissionGrantIds, payloadPermissionGrantIds)) {
            throw new DwnError(DwnErrorCode.MessagePermissionGrantIdsDescriptorPayloadMismatch, 'permission grant invocation in descriptor does not match signature payload');
        }
    }
    static areStringArraysEqual(a, b) {
        return a.length === b.length && a.every((value, index) => value === b[index]);
    }
}
//# sourceMappingURL=message.js.map