"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtendedApisUtilsImpl = void 0;
const utils_1 = require("../utils");
const SecurityMetadataDecryptor_1 = require("./SecurityMetadataDecryptor");
const SecureDelegation_1 = require("../../icc-api/model/SecureDelegation");
const EntityShareRequest_1 = require("../../icc-api/model/requests/EntityShareRequest");
const ShareResult_1 = require("../utils/ShareResult");
const ShareMetadataBehaviour_1 = require("./ShareMetadataBehaviour");
var AccessLevel = SecureDelegation_1.SecureDelegation.AccessLevelEnum;
var RequestedPermissionEnum = EntityShareRequest_1.EntityShareRequest.RequestedPermissionEnum;
var RequestedPermissionInternal = EntityShareRequest_1.EntityShareRequest.RequestedPermissionInternal;
var AccessLevelEnum = SecureDelegation_1.SecureDelegation.AccessLevelEnum;
const SecretIdUseOption_1 = require("./SecretIdUseOption");
/**
 * @internal this class is for internal use only and may be changed without notice.
 * Methods to support extended apis.
 */
class ExtendedApisUtilsImpl {
    constructor(primitives, dataOwnerApi, securityMetadataDecryptor, secureDelegationsManager, userApi, useParentKeys) {
        this.primitives = primitives;
        this.dataOwnerApi = dataOwnerApi;
        this.securityMetadataDecryptor = securityMetadataDecryptor;
        this.secureDelegationsManager = secureDelegationsManager;
        this.userApi = userApi;
        this.useParentKeys = useParentKeys;
    }
    encryptionKeysOf(entity, dataOwnerId) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.decryptAndMergeHierarchy(dataOwnerId, (hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey));
        });
    }
    encryptionKeysForHcpHierarchyOf(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.decryptHierarchy((hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey));
        });
    }
    secretIdsOf(entity, dataOwnerId) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.decryptAndMergeHierarchy(dataOwnerId, (hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.SecretId));
        });
    }
    secretIdsForHcpHierarchyOf(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.decryptHierarchy((hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.SecretId));
        });
    }
    owningEntityIdsOf(entity, dataOwnerId) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.decryptAndMergeHierarchy(dataOwnerId, (hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.OwningEntityId));
        });
    }
    owningEntityIdsForHcpHierarchyOf(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.decryptHierarchy((hierarchy) => this.securityMetadataDecryptor.decryptAll(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.OwningEntityId));
        });
    }
    hasWriteAccess(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return ((yield this.securityMetadataDecryptor.getEntityAccessLevel(entity.entity, yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds())) ===
                AccessLevel.WRITE);
        });
    }
    entityWithInitialisedEncryptedMetadata(entity, entityType, owningEntity, owningEntitySecretIds, initialiseEncryptionKey, autoDelegations, alternateRootDelegation) {
        var arguments_1 = arguments;
        return __awaiter(this, void 0, void 0, function* () {
            this.throwDetailedExceptionForInvalidParameter('entity.id', entity.id, 'entityWithInitialisedEncryptedMetadata', arguments_1);
            this.checkEmptyEncryptionMetadata(entity);
            const newRawKey = initialiseEncryptionKey ? yield this.primitives.AES.generateCryptoKey(true) : undefined;
            const newSecretId = this.primitives.randomUuid();
            return {
                updatedEntity: yield this.secureDelegationsManager.entityWithInitialisedEncryptedMetadata(Object.assign(Object.assign({}, entity), { secretForeignKeys: owningEntitySecretIds !== null && owningEntitySecretIds !== void 0 ? owningEntitySecretIds : [] }), entityType, newSecretId ? [newSecretId] : [], !!owningEntity ? [owningEntity] : [], newRawKey ? [newRawKey] : [], autoDelegations, alternateRootDelegation),
                rawEncryptionKey: newRawKey,
                secretId: newSecretId,
            };
        });
    }
    bulkShareOrUpdateEncryptedEntityMetadata(entitiesType, entitiesUpdates, doRequestBulkShareOrUpdate) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const { allRequestsByEntityId, orderedRequestsInfoByEntityId, unmodifiedEntitiesIds } = yield this.prepareBulkShareRequests(entitiesType, entitiesUpdates);
            const results = Object.keys(allRequestsByEntityId).length > 0 ? yield doRequestBulkShareOrUpdate({ requestsByEntityId: allRequestsByEntityId }) : [];
            const updatedEntities = [];
            const updateErrors = [];
            for (const result of results) {
                if (result.updatedEntity) {
                    updatedEntities.push(result.updatedEntity);
                }
                for (const [errorRequestId, error] of Object.entries((_a = result.rejectedRequests) !== null && _a !== void 0 ? _a : {})) {
                    const requestIndex = Number(errorRequestId);
                    const { delegateId, request, updatedForMigration } = orderedRequestsInfoByEntityId[result.entityId][requestIndex];
                    updateErrors.push({
                        entityId: result.entityId,
                        delegateId,
                        request,
                        updatedForMigration,
                        code: error.code,
                        reason: error.reason,
                    });
                }
            }
            // TODO implement auto-retry for failed requests if the shouldRetry flag in result.rejectedRequests is set to true
            return {
                updatedEntities,
                updateErrors,
                unmodifiedEntitiesIds,
            };
        });
    }
    bulkShareOrUpdateEncryptedEntityMetadataNoEntities(entitiesType, entitiesUpdates, doRequestBulkShareOrUpdate) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const { allRequestsByEntityId, orderedRequestsInfoByEntityId, unmodifiedEntitiesIds } = yield this.prepareBulkShareRequests(entitiesType, entitiesUpdates);
            const results = yield doRequestBulkShareOrUpdate({ requestsByEntityId: allRequestsByEntityId });
            const updateErrors = [];
            for (const result of results) {
                for (const [errorRequestId, error] of Object.entries((_a = result.rejectedRequests) !== null && _a !== void 0 ? _a : {})) {
                    const requestIndex = Number(errorRequestId);
                    const { delegateId, request, updatedForMigration } = orderedRequestsInfoByEntityId[result.entityId][requestIndex];
                    updateErrors.push({
                        entityId: result.entityId,
                        delegateId,
                        request,
                        updatedForMigration,
                        code: error.code,
                        reason: error.reason,
                    });
                }
            }
            const successfulRequests = Object.entries(allRequestsByEntityId)
                .flatMap(([entityId, requests]) => Object.keys(requests).map((delegateId) => ({ entityId, delegateId })))
                .filter(({ entityId, delegateId }) => !updateErrors.some((error) => error.entityId === entityId && error.delegateId === delegateId));
            return {
                successfulUpdates: successfulRequests,
                updateErrors,
                unmodifiedEntitiesIds,
            };
        });
    }
    prepareBulkShareRequests(entitiesType, entitiesUpdates) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c;
            if (new Set(entitiesUpdates.map((e) => e.entity.id)).size !== entitiesUpdates.length) {
                throw new Error('Duplicate requests: the same entity id is present more than once in the input');
            }
            const allRequestsByEntityId = {};
            const orderedRequestsInfoByEntityId = {};
            const unmodifiedEntitiesIds = [];
            for (const { entity, dataForDelegates } of entitiesUpdates) {
                const entityId = entity.id;
                if (!entityId)
                    throw new Error('Share of an entity requires for the entity to already exist and have an id');
                const entityWithType = { entity, type: entitiesType };
                const currentRequests = {};
                const currentOrderedRequests = [];
                const migrationRequests = yield this.makeMigrationRequestsIfNeeded(entityWithType, dataForDelegates);
                for (const [delegate, request] of Object.entries(migrationRequests)) {
                    currentRequests[String(currentOrderedRequests.length)] = request;
                    currentOrderedRequests.push({
                        delegateId: delegate,
                        request: dataForDelegates[delegate],
                        updatedForMigration: true,
                    });
                }
                for (const [delegate, userRequest] of Object.entries(dataForDelegates)) {
                    if (!migrationRequests[delegate]) {
                        const request = yield this.secureDelegationsManager.makeShareOrUpdateRequestParams(entityWithType, delegate, (_a = userRequest.shareSecretIds) !== null && _a !== void 0 ? _a : [], (_b = userRequest.shareEncryptionKeys) !== null && _b !== void 0 ? _b : [], (_c = userRequest.shareOwningEntityIds) !== null && _c !== void 0 ? _c : [], userRequest.requestedPermissions);
                        if (request) {
                            currentRequests[String(currentOrderedRequests.length)] = request;
                            currentOrderedRequests.push({
                                delegateId: delegate,
                                request: userRequest,
                                updatedForMigration: false,
                            });
                        }
                    }
                }
                if (Object.keys(currentRequests).length > 0) {
                    const existingDelegationMembersDetails = yield this.securityMetadataDecryptor.getDelegationMemberDetails(entityWithType);
                    const accessibleMembers = new Set(this.useParentKeys ? yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds() : [yield this.dataOwnerApi.getCurrentDataOwnerId()]);
                    const potentialParentDelegations = Object.entries(existingDelegationMembersDetails).flatMap(([k, members]) => {
                        if ((!!members.delegate && accessibleMembers.has(members.delegate)) || (!!members.delegator && accessibleMembers.has(members.delegator))) {
                            return [k];
                        }
                        else
                            return [];
                    });
                    allRequestsByEntityId[entityId] = { requests: currentRequests, potentialParentDelegations };
                    orderedRequestsInfoByEntityId[entityId] = currentOrderedRequests;
                }
                else {
                    unmodifiedEntitiesIds.push(entityId);
                }
            }
            return { unmodifiedEntitiesIds, allRequestsByEntityId, orderedRequestsInfoByEntityId };
        });
    }
    simpleShareOrUpdateEncryptedEntityMetadata(entity, delegates, doRequestBulkShareOrUpdate) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b;
            const availableEncryptionKeys = yield this.encryptionKeysOf(entity);
            const availableOwningEntityIds = yield this.owningEntityIdsOf(entity);
            const dataForDelegates = {};
            for (const [delegateId, delegateRequests] of Object.entries(delegates)) {
                if (!availableEncryptionKeys.length && delegateRequests.shareEncryptionKeys === ShareMetadataBehaviour_1.ShareMetadataBehaviour.REQUIRED) {
                    throw new Error(`Entity ${JSON.stringify(entity)} has no encryption keys or the current data owner can't access any encryption keys, but sharing is required.`);
                }
                if (!availableOwningEntityIds.length && delegateRequests.shareOwningEntityIds === ShareMetadataBehaviour_1.ShareMetadataBehaviour.REQUIRED) {
                    throw new Error(`Entity ${JSON.stringify(entity)} has no owning entity ids or the current data owner can't access any owning entity ids, but sharing is required.`);
                }
                dataForDelegates[delegateId] = {
                    shareSecretIds: (_a = delegateRequests.shareSecretIds) !== null && _a !== void 0 ? _a : (yield this.secretIdsOf(entity)),
                    shareEncryptionKeys: delegateRequests.shareEncryptionKeys === ShareMetadataBehaviour_1.ShareMetadataBehaviour.NEVER ? [] : availableEncryptionKeys,
                    shareOwningEntityIds: delegateRequests.shareOwningEntityIds === ShareMetadataBehaviour_1.ShareMetadataBehaviour.NEVER ? [] : availableOwningEntityIds,
                    requestedPermissions: (_b = delegateRequests.requestedPermissions) !== null && _b !== void 0 ? _b : RequestedPermissionEnum.MAX_WRITE,
                };
            }
            const shareResult = yield this.bulkShareOrUpdateEncryptedEntityMetadata(entity.type, [
                {
                    entity: entity.entity,
                    dataForDelegates,
                },
            ], (x) => doRequestBulkShareOrUpdate(x));
            if (shareResult.unmodifiedEntitiesIds.includes(entity.entity.id)) {
                return new ShareResult_1.ShareResultSuccess(entity.entity);
            }
            if (!shareResult.updateErrors.length && shareResult.updatedEntities.length === 1) {
                return new ShareResult_1.ShareResultSuccess(shareResult.updatedEntities[0]);
            }
            const requestedDelegates = new Set(Object.keys(delegates));
            const errorsOfRequestedDelegates = shareResult.updateErrors.filter((x) => requestedDelegates.has(x.delegateId));
            if (errorsOfRequestedDelegates.length === 0 && shareResult.updatedEntities.length === 1) {
                console.warn(`Errors with migration of encrypted metadata ${JSON.stringify(shareResult.updateErrors)}.`);
                return new ShareResult_1.ShareResultSuccess(shareResult.updatedEntities[0]);
            }
            return new ShareResult_1.ShareResultFailure(shareResult.updateErrors, `There was an error sharing entity with id ${entity.entity.id}. Check the logs for more details.`);
        });
    }
    makeMigrationRequestsIfNeeded(entity, userRequestsForEntity) {
        return __awaiter(this, void 0, void 0, function* () {
            const hierarchy = this.useParentKeys
                ? yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds()
                : [yield this.dataOwnerApi.getCurrentDataOwnerId()];
            const legacySecretIds = yield this.securityMetadataDecryptor.decryptAllLegacyDelegations(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.SecretId);
            const legacyEncryptionKeys = yield this.securityMetadataDecryptor.decryptAllLegacyDelegations(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey);
            const legacyOwningEntityIds = yield this.securityMetadataDecryptor.decryptAllLegacyDelegations(entity.entity, hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.OwningEntityId);
            const res = {};
            const selfId = yield this.dataOwnerApi.getCurrentDataOwnerId();
            for (const hierarchyMember of hierarchy) {
                const hierarchyMemberMigration = yield this.makeMigrationRequestForMemberOfHierarchy(entity, selfId, hierarchyMember, userRequestsForEntity[hierarchyMember], legacySecretIds, legacyEncryptionKeys, legacyOwningEntityIds);
                if (hierarchyMemberMigration) {
                    res[hierarchyMember] = hierarchyMemberMigration;
                }
            }
            return res;
        });
    }
    makeMigrationRequestForMemberOfHierarchy(entity, selfId, currMemberId, userRequestForDelegate, legacySecretIds, legacyEncryptionKeys, legacyOwningEntityIds) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c;
            // This implementation is very specific from migration from delegations to secure delegations. If in future we will have to migrate from secure delegations to something else, this method may need significant changes in its logic.
            const subHierarchy = this.useParentKeys ? yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIdsFrom(currMemberId) : [currMemberId];
            const subHierarchySet = new Set(subHierarchy);
            const legacyAccess = selfId === entity.entity.id && currMemberId === selfId
                ? AccessLevel.WRITE
                : yield this.securityMetadataDecryptor.getEntityLegacyDelegationAccessLevel(entity.entity, subHierarchy);
            if (!legacyAccess)
                return undefined;
            const selfLegacySecretIds = legacySecretIds.filter((x) => x.dataOwnersWithAccess.some((d) => subHierarchySet.has(d))).map((x) => x.decrypted);
            const selfLegacyEncryptionKeys = legacyEncryptionKeys
                .filter((x) => x.dataOwnersWithAccess.some((d) => subHierarchySet.has(d)))
                .map((x) => x.decrypted);
            const selfLegacyOwningEntityIds = legacyOwningEntityIds
                .filter((x) => x.dataOwnersWithAccess.some((d) => subHierarchySet.has(d)))
                .map((x) => x.decrypted);
            let missingSecretIds = [];
            let missingEncryptionKeys = [];
            let missingOwningEntityIds = [];
            if (selfLegacySecretIds.length > 0) {
                const currentSecretIds = new Set((yield this.securityMetadataDecryptor.decryptAllSecureDelegations(entity.entity, [currMemberId], SecurityMetadataDecryptor_1.SecurityMetadataType.SecretId)).map((x) => x.decrypted));
                missingSecretIds = selfLegacySecretIds.filter((x) => !currentSecretIds.has(x));
            }
            if (selfLegacyEncryptionKeys.length > 0) {
                const currentEncryptionKeys = new Set((yield this.securityMetadataDecryptor.decryptAllSecureDelegations(entity.entity, [currMemberId], SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey)).map((x) => x.decrypted));
                missingEncryptionKeys = selfLegacyEncryptionKeys.filter((x) => !currentEncryptionKeys.has(x));
            }
            if (selfLegacyOwningEntityIds.length > 0) {
                const currentOwningEntityIds = new Set((yield this.securityMetadataDecryptor.decryptAllSecureDelegations(entity.entity, [currMemberId], SecurityMetadataDecryptor_1.SecurityMetadataType.OwningEntityId)).map((x) => x.decrypted));
                missingOwningEntityIds = selfLegacyOwningEntityIds.filter((x) => !currentOwningEntityIds.has(x));
            }
            const mustCreateRootDelegation = selfId === entity.entity.id &&
                currMemberId === selfId &&
                !(yield this.securityMetadataDecryptor.getEntitySecureDelegationAccessLevel(entity.entity, subHierarchy));
            if (missingSecretIds.length > 0 || missingEncryptionKeys.length > 0 || missingOwningEntityIds.length > 0 || mustCreateRootDelegation) {
                let requestedPermissions;
                if (currMemberId === selfId) {
                    requestedPermissions = RequestedPermissionInternal.ROOT;
                }
                else {
                    requestedPermissions = RequestedPermissionInternal.FULL_WRITE; // Legacy permission if present is always write
                }
                return yield this.secureDelegationsManager.makeShareOrUpdateRequestParams(entity, currMemberId, Array.from(new Set([...missingSecretIds, ...((_a = userRequestForDelegate === null || userRequestForDelegate === void 0 ? void 0 : userRequestForDelegate.shareSecretIds) !== null && _a !== void 0 ? _a : [])])), Array.from(new Set([...missingEncryptionKeys, ...((_b = userRequestForDelegate === null || userRequestForDelegate === void 0 ? void 0 : userRequestForDelegate.shareEncryptionKeys) !== null && _b !== void 0 ? _b : [])])), Array.from(new Set([...missingOwningEntityIds, ...((_c = userRequestForDelegate === null || userRequestForDelegate === void 0 ? void 0 : userRequestForDelegate.shareOwningEntityIds) !== null && _c !== void 0 ? _c : [])])), requestedPermissions);
            }
            else
                return undefined;
        });
    }
    tryDecryptDataOf(entity, content, postProcessor) {
        return __awaiter(this, void 0, void 0, function* () {
            const triedKeys = new Set();
            const result = yield this.doIncrementallyDecryptingKeys(entity.entity, entity.type, (e, t, keys) => __awaiter(this, void 0, void 0, function* () {
                for (const k of keys) {
                    if (!triedKeys.has(k.raw)) {
                        triedKeys.add(k.raw);
                        try {
                            const decrypted = yield this.primitives.AES.decrypt(k.key, content);
                            if (postProcessor) {
                                const processed = yield postProcessor(decrypted);
                                if (processed !== undefined)
                                    return { success: processed };
                            }
                            else {
                                return { success: decrypted };
                            }
                        }
                        catch (e) {
                            console.warn(`Error while attempting to decrypt attachment of ${entity.entity.id} with raw key ${k.raw}: ${e}`);
                        }
                    }
                }
                return null;
            }));
            if (result != null) {
                return { data: result.success, wasDecrypted: true };
            }
            else {
                return { data: (0, utils_1.ua2ab)(content), wasDecrypted: false };
            }
        });
    }
    encryptDataOf(entity, type, content, saveEntity) {
        return __awaiter(this, void 0, void 0, function* () {
            const ensureInitialisedKeysResult = yield this.ensureEncryptionKeysInitialised(entity, type);
            let updatedEntity;
            if (!!ensureInitialisedKeysResult) {
                updatedEntity = yield saveEntity(ensureInitialisedKeysResult);
            }
            const encrypted = yield this.doIncrementallyDecryptingKeys(entity, type, (e, t, keys) => __awaiter(this, void 0, void 0, function* () {
                for (const k of keys) {
                    try {
                        return {
                            success: {
                                encryptedData: yield this.primitives.AES.encrypt(k.key, content),
                                updatedEntity: updatedEntity,
                            },
                        };
                    }
                    catch (e) {
                        console.warn(`Error while encrypting with raw key ${k.raw}: ${e}`);
                    }
                }
                return null;
            }));
            if (encrypted != null)
                return encrypted.success;
            throw new Error(`Could not extract any valid encryption keys for entity ${JSON.stringify(entity)}.`);
        });
    }
    tryDecryptEntities(entities, entityType, constructor) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const nothingToDecryptResults = new Map();
            for (const entity of entities) {
                const nothingToDecrypt = yield (0, utils_1.decryptObject)(entity, (encrypted) => __awaiter(this, void 0, void 0, function* () {
                    return null;
                }));
                if (nothingToDecrypt != null) {
                    nothingToDecryptResults.set(entity.id, constructor(nothingToDecrypt));
                }
            }
            const actuallyDecryptedResults = yield this.doManyIncrementallyDecryptingKeys(entities.filter((e) => !nothingToDecryptResults.has(e.id)), entityType, (entity, t, keys) => __awaiter(this, void 0, void 0, function* () {
                // The decrypt object will try all keys on each of the sub-objects; this is intentional because even though it
                // shouldn't happen, but it is still possible that some entity could be accidentally merged badly and the merged
                // entity has multiple encryptedSelf (on different sub-entities) that use different keys.
                const decrypted = yield (0, utils_1.decryptObject)(entity, (encrypted) => this.tryDecryptJson(keys, encrypted, false));
                if (decrypted != null) {
                    return { success: constructor(decrypted) };
                }
                else {
                    return null;
                }
            }));
            const res = [];
            for (const entity of entities) {
                const decrypted = (_a = actuallyDecryptedResults.get(entity.id)) !== null && _a !== void 0 ? _a : nothingToDecryptResults.get(entity.id);
                if (!decrypted) {
                    res.push({ entity: entity, decrypted: false });
                }
                else {
                    res.push({ entity: decrypted, decrypted: true });
                }
            }
            return res;
        });
    }
    tryDecryptJson(potentialKeys, encrypted, truncateTrailingDecryptedNulls) {
        return __awaiter(this, void 0, void 0, function* () {
            for (const key of potentialKeys) {
                try {
                    const decrypted = yield this.primitives.AES.decrypt(key.key, encrypted, key.raw);
                    return JSON.parse((0, utils_1.ua2utf8)(truncateTrailingDecryptedNulls ? (0, utils_1.truncateTrailingNulls)(new Uint8Array(decrypted)) : decrypted));
                }
                catch (e) { }
            }
            return null;
        });
    }
    tryEncryptEntities(entities, entityType, fieldsToEncrypt, encodeBinaryData, requireEncryption, constructor) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const entitiesWithInitialisedEncryptionKeys = [];
            for (const entity of entities) {
                entitiesWithInitialisedEncryptionKeys.push((_a = (yield this.ensureEncryptionKeysInitialised(entity, entityType))) !== null && _a !== void 0 ? _a : entity);
            }
            const results = yield this.doManyIncrementallyDecryptingKeys(entitiesWithInitialisedEncryptionKeys, entityType, (e, t, keys) => __awaiter(this, void 0, void 0, function* () {
                for (const k of keys) {
                    try {
                        const encrypted = yield (0, utils_1.encryptObject)(e, (obj) => {
                            // TODO encoding of binary data should probably be applied to everything?
                            const json = encodeBinaryData
                                ? JSON.stringify(obj, (k, v) => {
                                    return v instanceof ArrayBuffer || ArrayBuffer.isView(v)
                                        ? (0, utils_1.b2a)(new Uint8Array(v).reduce((d, b) => d + String.fromCharCode(b), ''))
                                        : v;
                                })
                                : JSON.stringify(obj);
                            return this.primitives.AES.encrypt(k.key, (0, utils_1.utf8_2ua)(json), k.raw);
                        }, (encryptedSelf) => this.tryDecryptJson(keys, encryptedSelf, false), fieldsToEncrypt, entityType);
                        return { success: constructor(encrypted) };
                    }
                    catch (e) {
                        console.warn(`Error while encrypting with raw key ${k.raw}: ${e}`);
                    }
                }
                return null;
            }));
            if (results.size != entitiesWithInitialisedEncryptionKeys.length) {
                if (requireEncryption) {
                    throw new Error(`Could not encrypt entities ${entitiesWithInitialisedEncryptionKeys.flatMap((e) => (results.has(e.id) ? [] : [e.id]))}`);
                }
                else {
                    for (const e of entitiesWithInitialisedEncryptionKeys) {
                        if (!results.has(e.id)) {
                            yield (0, utils_1.encryptObject)(e, (obj) => __awaiter(this, void 0, void 0, function* () {
                                const hasNonEmptyValues = Object.values(obj).some((v) => v !== undefined && (typeof v !== 'object' || (Array.isArray(v) && v.length > 0) || Object.keys(v).length > 0));
                                if (hasNonEmptyValues) {
                                    throw new Error(`Impossible to modify encrypted content of an entity if no encryption key is known.\nEntity: ${JSON.stringify(e)}\nTo encrypt: ${JSON.stringify(obj)}`);
                                }
                                return Promise.resolve(new ArrayBuffer(1));
                            }), () => Promise.resolve(null), fieldsToEncrypt, 'entity');
                            results.set(e.id, e);
                        }
                    }
                }
            }
            return entitiesWithInitialisedEncryptionKeys.map((e) => results.get(e.id));
        });
    }
    ensureEncryptionKeysInitialised(entity, entityType) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a, _b, _c;
            if (this.securityMetadataDecryptor.hasAnyEncryptionKeys(entity))
                return undefined;
            if (!entity.rev) {
                throw new Error('New encrypted entity is lacking encryption metadata. ' +
                    'Please instantiate new entities using the `newInstance` method from the respective extended api.');
            }
            /*
             * Add encryption key and share with all auto-delegates already in legacy delegations.
             * TODO disable this logic and simply throw error for post-2018 customers.
             */
            const existingDelegations = new Set(Object.keys((_a = entity.delegations) !== null && _a !== void 0 ? _a : {}));
            const usersWithAccessToNewKey = Object.fromEntries(Object.values((_b = (yield this.userApi.getCurrentUser()).autoDelegations) !== null && _b !== void 0 ? _b : {})
                .flatMap((x) => x)
                .filter((x) => existingDelegations.has(x))
                .map((x) => [x, AccessLevelEnum.WRITE]));
            return yield this.secureDelegationsManager.entityWithInitialisedEncryptedMetadata(Object.assign(Object.assign({}, entity), { secretForeignKeys: (_c = entity.secretForeignKeys) !== null && _c !== void 0 ? _c : [] }), entityType, [], [], [yield this.primitives.AES.generateCryptoKey(true)], usersWithAccessToNewKey, undefined);
        });
    }
    decryptHierarchy(decryptedDataProvider) {
        return __awaiter(this, void 0, void 0, function* () {
            const canDecryptOwnerIds = this.useParentKeys
                ? yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds()
                : [yield this.dataOwnerApi.getCurrentDataOwnerId()];
            return yield decryptedDataProvider(canDecryptOwnerIds);
        });
    }
    decryptAndMergeHierarchy(dataOwnerId, decryptedDataProvider) {
        return __awaiter(this, void 0, void 0, function* () {
            const hierarchy = this.useParentKeys
                ? dataOwnerId
                    ? yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIdsFrom(dataOwnerId)
                    : yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds()
                : [dataOwnerId !== null && dataOwnerId !== void 0 ? dataOwnerId : (yield this.dataOwnerApi.getCurrentDataOwnerId())];
            const decryptedData = yield decryptedDataProvider(hierarchy);
            const merged = decryptedData.flatMap((x) => x.extracted);
            return this.deduplicate(merged);
        });
    }
    tryImportKey(key) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!/^[0-9A-Fa-f\-]+$/g.test(key))
                return undefined;
            try {
                return yield this.primitives.AES.importKey('raw', (0, utils_1.hex2ua)(key.replace(/-/g, '')));
            }
            catch (e) {
                console.warn(`Could not import key ${key} as an encryption key.`, e);
                return undefined;
            }
        });
    }
    doIncrementallyDecryptingKeys(entity, entityType, action) {
        return __awaiter(this, void 0, void 0, function* () {
            const res = yield this.doManyIncrementallyDecryptingKeys([entity], entityType, (e, t, ks) => action(e, t, ks));
            if (res.has(entity.id)) {
                return { success: res.get(entity.id) };
            }
            else {
                return null;
            }
        });
    }
    doManyIncrementallyDecryptingKeys(entities, entitiesType, action) {
        return __awaiter(this, void 0, void 0, function* () {
            if (entities.length == 0)
                return new Map();
            let deduplicatedEntities = entities;
            if (new Set(deduplicatedEntities.map((e) => e.id)).size != deduplicatedEntities.length) {
                console.error(`Duplicate entries in entities ${deduplicatedEntities.map((x) => x.id)}`);
                deduplicatedEntities = this.deduplicateById(deduplicatedEntities);
            }
            const hierarchy = yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIds();
            const allExtractedKeysForEntities = Object.fromEntries(deduplicatedEntities.map((x) => [x.id, new Set()]));
            const newlyExtractedKeysForEntities = Object.fromEntries(deduplicatedEntities.map((x) => [x.id, new Set()]));
            const results = new Map();
            const remainingEntitiesById = Object.fromEntries(deduplicatedEntities.map((x) => [x.id, x]));
            const importedKeysByRaw = new Map();
            const primitives = this.primitives;
            function updateExtractedKeysAndDoActionIfNecessary(newKeys, forceUpdateOnEntitiesWithNewExtractedKeys) {
                return __awaiter(this, void 0, void 0, function* () {
                    for (const [entityId, keys] of Object.entries(newKeys)) {
                        const newlyExtractedSet = newlyExtractedKeysForEntities[entityId];
                        const alreadyExtractedSet = allExtractedKeysForEntities[entityId];
                        for (const k of keys) {
                            if (!alreadyExtractedSet.has(k)) {
                                newlyExtractedSet.add(k);
                                if (!importedKeysByRaw.has(k)) {
                                    importedKeysByRaw.set(k, yield primitives.AES.importKey('raw', (0, utils_1.hex2ua)(k)));
                                }
                            }
                        }
                    }
                    if (forceUpdateOnEntitiesWithNewExtractedKeys ||
                        Object.keys(remainingEntitiesById).every((eId) => newlyExtractedKeysForEntities[eId].size > 0)) {
                        for (const entity of Object.values(remainingEntitiesById)) {
                            const currId = entity.id;
                            const currNewlyExtracted = newlyExtractedKeysForEntities[currId];
                            if (currNewlyExtracted.size > 0) {
                                const currAllExtracted = allExtractedKeysForEntities[currId];
                                currNewlyExtracted.forEach((k) => currAllExtracted.add(k));
                                currNewlyExtracted.clear();
                                const actionResult = yield action(entity, entitiesType, [...currAllExtracted].map((raw) => ({
                                    raw: raw,
                                    key: importedKeysByRaw.get(raw),
                                })));
                                if (actionResult != null) {
                                    delete remainingEntitiesById[currId];
                                    results.set(currId, actionResult.success);
                                }
                            }
                        }
                    }
                });
            }
            yield updateExtractedKeysAndDoActionIfNecessary(yield this.securityMetadataDecryptor.decryptLegacyDelegations(Object.values(remainingEntitiesById), hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey), false);
            if (Object.keys(remainingEntitiesById).length == 0)
                return results;
            yield updateExtractedKeysAndDoActionIfNecessary(yield this.securityMetadataDecryptor.decryptSecureDelegationsUsingCache(Object.values(remainingEntitiesById), hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey), false);
            if (Object.keys(remainingEntitiesById).length == 0)
                return results;
            yield updateExtractedKeysAndDoActionIfNecessary(yield this.securityMetadataDecryptor.decryptSecureDelegationsUsingKnownExchangeData(Object.values(remainingEntitiesById), hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey), false);
            if (Object.keys(remainingEntitiesById).length == 0)
                return results;
            yield updateExtractedKeysAndDoActionIfNecessary(yield this.securityMetadataDecryptor.decryptSecureDelegationsUsingExchangeDataMap(Object.values(remainingEntitiesById), hierarchy, SecurityMetadataDecryptor_1.SecurityMetadataType.EncryptionKey), false);
            if (Object.keys(remainingEntitiesById).length == 0)
                return results;
            yield updateExtractedKeysAndDoActionIfNecessary({}, true);
            return results;
        });
    }
    deduplicate(values) {
        return [...new Set(values)];
    }
    throwDetailedExceptionForInvalidParameter(argName, argValue, methodName, methodArgs) {
        if (argValue)
            return;
        let details = '\nMethod name: icc-crypto-x-api.' + methodName + '()\nArguments:';
        if (methodArgs) {
            try {
                const argsArray = [...methodArgs];
                argsArray.forEach((arg, index) => (details += '\n[' + index + ']: ' + JSON.stringify(arg)));
            }
            catch (ex) {
                details += '; a problem occured while logging arguments details: ' + ex;
            }
        }
        throw new Error('### THIS SHOULD NOT HAPPEN: ' + argName + ' has an invalid value: ' + argValue + details);
    }
    checkEmptyEncryptionMetadata(entity) {
        this.doCheckEmptyEncryptionMetadata(entity, true);
    }
    hasEmptyEncryptionMetadata(entity) {
        return this.doCheckEmptyEncryptionMetadata(entity, false);
    }
    doCheckEmptyEncryptionMetadata(entity, throwErrorIfNonEmpty) {
        const existingMetadata = [];
        if (entity.delegations && Object.keys(entity.delegations).length)
            existingMetadata.push('delegations');
        if (entity.cryptedForeignKeys && Object.keys(entity.cryptedForeignKeys).length)
            existingMetadata.push('cryptedForeignKeys');
        if (entity.encryptionKeys && Object.keys(entity.encryptionKeys).length)
            existingMetadata.push('encryptionKeys');
        if (entity.secretForeignKeys && entity.secretForeignKeys.length)
            existingMetadata.push('secretForeignKeys');
        if (entity.securityMetadata && Object.keys(entity.securityMetadata).length)
            existingMetadata.push('securityMetadata');
        if (existingMetadata.length > 0) {
            if (throwErrorIfNonEmpty) {
                throw new Error(`Entity should have no encryption metadata on initialisation, but the following fields already have some values: ${existingMetadata}\n` +
                    JSON.stringify(entity, undefined, 2));
            }
            else
                return false;
        }
        return true;
    }
    initialiseConfidentialSecretId(entity, entityType, doRequestBulkShareOrUpdate) {
        return __awaiter(this, void 0, void 0, function* () {
            if (yield this.getConfidentialSecretId({ entity, type: entityType }))
                return undefined;
            const confidentialSecretId = this.primitives.randomUuid();
            return (yield this.simpleShareOrUpdateEncryptedEntityMetadata({ entity, type: entityType }, {
                [yield this.dataOwnerApi.getCurrentDataOwnerId()]: {
                    shareEncryptionKeys: ShareMetadataBehaviour_1.ShareMetadataBehaviour.NEVER,
                    shareOwningEntityIds: ShareMetadataBehaviour_1.ShareMetadataBehaviour.NEVER,
                    shareSecretIds: [confidentialSecretId],
                    requestedPermissions: RequestedPermissionEnum.MAX_WRITE,
                },
            }, (request) => doRequestBulkShareOrUpdate(request))).updatedEntityOrThrow;
        });
    }
    getConfidentialSecretId(entity, dataOwnerId) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.getConfidentialSecretIds(entity, dataOwnerId).then((x) => x[0]);
        });
    }
    getConfidentialSecretIds(entity, dataOwnerId) {
        return __awaiter(this, void 0, void 0, function* () {
            const chosenDataOwnerId = dataOwnerId !== null && dataOwnerId !== void 0 ? dataOwnerId : (yield this.dataOwnerApi.getCurrentDataOwnerId());
            const dataOwnerHierarchy = yield this.dataOwnerApi.getCurrentDataOwnerHierarchyIdsFrom(chosenDataOwnerId);
            const hierarchySecretIds = (yield this.secretIdsForHcpHierarchyOf(entity)).filter((x) => dataOwnerHierarchy.includes(x.ownerId));
            const keysForDataOwner = hierarchySecretIds.find((x) => x.ownerId === chosenDataOwnerId);
            if (!keysForDataOwner)
                return [];
            return keysForDataOwner.extracted.filter((k) => !hierarchySecretIds.some((x) => x.ownerId !== chosenDataOwnerId && x.extracted.includes(k)));
        });
    }
    getAnySecretIdSharedWithParents(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return (yield this.getSecretIdsSharedWithParents(entity))[0];
        });
    }
    getSecretIdsSharedWithParents(entity) {
        return __awaiter(this, void 0, void 0, function* () {
            return (yield this.secretIdsForHcpHierarchyOf(entity))[0].extracted;
        });
    }
    resolveSecretIdUseOptions(entity, option) {
        return __awaiter(this, void 0, void 0, function* () {
            if (option == SecretIdUseOption_1.SecretIdUseOption.UseNone) {
                return [];
            }
            else if (option == SecretIdUseOption_1.SecretIdUseOption.UseAnyConfidential) {
                const all = yield this.getConfidentialSecretIds(entity, undefined);
                if (all.length == 0)
                    throw new Error("Couldn't find any confidential secret id");
                return [all[0]];
            }
            else if (option == SecretIdUseOption_1.SecretIdUseOption.UseAllConfidential) {
                const all = yield this.getConfidentialSecretIds(entity, undefined);
                if (all.length == 0)
                    throw new Error("Couldn't find any confidential secret id");
                return all;
            }
            else if (option == SecretIdUseOption_1.SecretIdUseOption.UseAnySharedWithParent) {
                const all = yield this.getSecretIdsSharedWithParents(entity);
                if (all.length == 0)
                    throw new Error("Couldn't find any secret id shared with parent");
                return [all[0]];
            }
            else if (option == SecretIdUseOption_1.SecretIdUseOption.UseAllSharedWithParent) {
                const all = yield this.getSecretIdsSharedWithParents(entity);
                if (all.length == 0)
                    throw new Error("Couldn't find any secret id shared with parent");
                return all;
            }
            else if (option instanceof SecretIdUseOption_1.SecretIdUseOption.Use) {
                return [...new Set(option.secretIds)];
            }
            else {
                throw new Error(`Unrecognized SecretIdUseOption ${option}`);
            }
        });
    }
    deduplicateById(entities) {
        const seen = new Set();
        return entities.filter((e) => {
            if (seen.has(e.id))
                return false;
            seen.add(e.id);
            return true;
        });
    }
}
exports.ExtendedApisUtilsImpl = ExtendedApisUtilsImpl;
//# sourceMappingURL=ExtendedApisUtilsImpl.js.map