"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.IccContactXApi = void 0;
const icc_api_1 = require("../icc-api");
const contact_i18n_1 = require("./rsrc/contact.i18n");
const date_fns_1 = require("date-fns");
const collection_utils_1 = require("./utils/collection-utils");
const models = require("../icc-api/model/models");
const models_1 = require("../icc-api/model/models");
const binary_utils_1 = require("./utils/binary-utils");
const ServiceByIdsFilter_1 = require("./filters/ServiceByIdsFilter");
const utils_1 = require("./utils");
const AuthenticationProvider_1 = require("./auth/AuthenticationProvider");
const SecureDelegation_1 = require("../icc-api/model/SecureDelegation");
const Connection_1 = require("../icc-api/model/Connection");
var AccessLevelEnum = SecureDelegation_1.SecureDelegation.AccessLevelEnum;
const SecretIdUseOption_1 = require("./crypto/SecretIdUseOption");
class IccContactXApi extends icc_api_1.IccContactApi {
    get headers() {
        return super.headers.then((h) => this.crypto.accessControlKeysHeaders.addAccessControlKeysHeaders(h, utils_1.EntityWithDelegationTypeName.Contact));
    }
    constructor(host, headers, crypto, dataOwnerApi, userApi, authApi, autofillAuthor, authenticationProvider = new AuthenticationProvider_1.NoAuthenticationProvider(), fetchImpl = typeof window !== 'undefined'
        ? window.fetch
        : typeof self !== 'undefined'
            ? self.fetch
            : fetch, contactEncryptedKeys = ['descr'], serviceEncryptedKeys = []) {
        super(host, headers, authenticationProvider, fetchImpl);
        this.crypto = crypto;
        this.dataOwnerApi = dataOwnerApi;
        this.userApi = userApi;
        this.authApi = authApi;
        this.autofillAuthor = autofillAuthor;
        this.i18n = contact_i18n_1.default;
        if (contactEncryptedKeys.some((key) => key.startsWith('services'))) {
            throw new Error("You can't customise encryption of the `services` field of Contact. Use the serviceEncryptedKeys parameter instead.");
        }
        if (serviceEncryptedKeys.some((key) => key.startsWith('content'))) {
            throw new Error("You can't customise encryption of the `content` of a Service. The content values for services is automatically encrypted.");
        }
        this.crypto = crypto;
        this.dataOwnerApi = dataOwnerApi;
        this.userApi = userApi;
        this.authApi = authApi;
        this.contactEncryptedFields = (0, utils_1.parseEncryptedFields)(contactEncryptedKeys, 'Contact.');
        const customServiceEncryptedFields = (0, utils_1.parseEncryptedFields)(serviceEncryptedKeys, 'Service.');
        if (serviceEncryptedKeys.length > 0) {
            this.serviceEncryptedFieldsNoContent = customServiceEncryptedFields;
        }
        this.serviceEncryptedFieldsWithContent = Object.assign(Object.assign({}, customServiceEncryptedFields), { topLevelFields: [...customServiceEncryptedFields.topLevelFields, { fieldName: 'content', fieldPath: 'Service.content' }] });
    }
    /**
     * Creates a new instance of contact with initialised encryption metadata (not in the database).
     * @param user the current user.
     * @param patient the patient this contact refers to.
     * @param c initialised data for the contact. Metadata such as id, creation data, etc. will be automatically initialised, but you can specify other
     * kinds of data or overwrite generated metadata with this. You can't specify encryption metadata.
     * @param options optional parameters:
     * - additionalDelegates: delegates which will have access to the entity in addition to the current data owner and delegates from the
     * auto-delegations. Must be an object which associates each data owner id with the access level to give to that data owner. May overlap with
     * auto-delegations, in such case the access level specified here will be used.
     * - sfkOption: specifies which sfk of the owning entity to use.
     * - ignoreAutoDelegations: if true the data won't be shared with the autodelegations of the user, but only with additional delegates
     * - alternateRootDelegation: by default a new entity is created with a root delegation from self to self. In keyless mode this is not possible,
     * and instead the root delegation will be from self to another. You have to specify which delegate will be part of the root delegation.
     * @return a new instance of contact.
     */
    newInstance(user_1, patient_1, c_1) {
        return __awaiter(this, arguments, void 0, function* (user, patient, c, options = {}) {
            var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
            const contact = new models.Contact(Object.assign(Object.assign({}, (c !== null && c !== void 0 ? c : {})), { _type: 'org.taktik.icure.entities.Contact', id: (_a = c === null || c === void 0 ? void 0 : c.id) !== null && _a !== void 0 ? _a : this.crypto.primitives.randomUuid(), created: (_b = c === null || c === void 0 ? void 0 : c.created) !== null && _b !== void 0 ? _b : new Date().getTime(), modified: (_c = c === null || c === void 0 ? void 0 : c.modified) !== null && _c !== void 0 ? _c : new Date().getTime(), responsible: (_d = c === null || c === void 0 ? void 0 : c.responsible) !== null && _d !== void 0 ? _d : (this.autofillAuthor ? this.dataOwnerApi.getDataOwnerIdOf(user) : undefined), author: (_e = c === null || c === void 0 ? void 0 : c.author) !== null && _e !== void 0 ? _e : (this.autofillAuthor ? user.id : undefined), codes: (_f = c === null || c === void 0 ? void 0 : c.codes) !== null && _f !== void 0 ? _f : [], tags: (_g = c === null || c === void 0 ? void 0 : c.tags) !== null && _g !== void 0 ? _g : [], groupId: (_h = c === null || c === void 0 ? void 0 : c.groupId) !== null && _h !== void 0 ? _h : this.crypto.primitives.randomUuid(), subContacts: (_j = c === null || c === void 0 ? void 0 : c.subContacts) !== null && _j !== void 0 ? _j : [], services: (_k = c === null || c === void 0 ? void 0 : c.services) !== null && _k !== void 0 ? _k : [], openingDate: (_l = c === null || c === void 0 ? void 0 : c.openingDate) !== null && _l !== void 0 ? _l : parseInt((0, date_fns_1.format)(new Date(), 'yyyyMMddHHmmss')) }));
            const ownerId = this.dataOwnerApi.getDataOwnerIdOf(user);
            if (ownerId !== (yield this.dataOwnerApi.getCurrentDataOwnerId()))
                throw new Error('Can only initialise entities as current data owner.');
            const sfk = yield this.crypto.xapi.resolveSecretIdUseOptions({ entity: patient, type: utils_1.EntityWithDelegationTypeName.Patient }, (_m = options.sfkOption) !== null && _m !== void 0 ? _m : SecretIdUseOption_1.SecretIdUseOption.UseAnySharedWithParent);
            const extraDelegations = Object.assign(Object.assign({}, (options.ignoreAutoDelegations == true
                ? {}
                : Object.fromEntries([...((_p = (_o = user.autoDelegations) === null || _o === void 0 ? void 0 : _o.all) !== null && _p !== void 0 ? _p : []), ...((_r = (_q = user.autoDelegations) === null || _q === void 0 ? void 0 : _q.medicalInformation) !== null && _r !== void 0 ? _r : [])].map((d) => [d, AccessLevelEnum.WRITE])))), ((_s = options === null || options === void 0 ? void 0 : options.additionalDelegates) !== null && _s !== void 0 ? _s : {}));
            const initialisationInfo = yield this.crypto.xapi.entityWithInitialisedEncryptedMetadata(contact, utils_1.EntityWithDelegationTypeName.Contact, patient.id, sfk, true, extraDelegations, options.alternateRootDelegation);
            return new models.Contact(initialisationInfo.updatedEntity);
        });
    }
    /**
     * 1. Check whether there is a delegation with 'hcpartyId' or not.
     * 2. 'fetchHcParty[hcpartyId][1]': is encrypted AES exchange key by RSA public key of him.
     * 3. Obtain the AES exchange key, by decrypting the previous step value with hcparty private key
     *      3.1.  KeyPair should be fetch from cache (in jwk)
     *      3.2.  if it doesn't exist in the cache, it has to be loaded from Browser Local store, and then import it to WebCrypto
     * 4. Obtain the array of delegations which are delegated to his ID (hcpartyId) in this patient
     * 5. Decrypt and collect all keys (secretForeignKeys) within delegations of previous step (with obtained AES key of step 4)
     * 6. Do the REST call to get all contacts with (allSecretForeignKeysDelimitedByComa, hcpartyId)
     *
     * After these painful steps, you have the contacts of the patient.
     *
     * @deprecated use {@link findIdsBy} instead.
     * @param hcpartyId
     * @param patient (Promise)
     * @param usingPost
     */
    findBy(hcpartyId_1, patient_1) {
        return __awaiter(this, arguments, void 0, function* (hcpartyId, patient, usingPost = false) {
            return yield this.crypto.xapi.secretIdsForHcpHierarchyOf({ entity: patient, type: utils_1.EntityWithDelegationTypeName.Patient }).then((keysHierarchy) => keysHierarchy && keysHierarchy.length > 0
                ? Promise.all(keysHierarchy
                    .reduce((acc, level) => {
                    return acc.concat([
                        {
                            hcpartyId: level.ownerId,
                            extractedKeys: level.extracted.filter((key) => !acc.some((previousLevel) => previousLevel.extractedKeys.includes(key))),
                        },
                    ]);
                }, [])
                    .filter((l) => l.extractedKeys.length > 0)
                    .map(({ hcpartyId, extractedKeys }) => usingPost
                    ? this.findByHCPartyPatientSecretFKeysArray(hcpartyId, [...new Set(extractedKeys)])
                    : this.findByHCPartyPatientSecretFKeys(hcpartyId, [...new Set(extractedKeys)].join(',')))).then((results) => (0, collection_utils_1.uniqBy)(results.flat(), (x) => x.id))
                : Promise.resolve([]));
        });
    }
    /**
     * Same as {@link findBy} but it will only return the ids of the contacts. It can also filter the contacts where Contact.openingDate is between
     * startDate and endDate in ascending or descending order by that field. (default: ascending).
     */
    findIdsBy(hcpartyId, patient, startDate, endDate, descending) {
        return __awaiter(this, void 0, void 0, function* () {
            return yield this.crypto.xapi.secretIdsForHcpHierarchyOf({ entity: patient, type: utils_1.EntityWithDelegationTypeName.Patient }).then((keysHierarchy) => keysHierarchy && keysHierarchy.length > 0
                ? Promise.all(keysHierarchy
                    .reduce((acc, level) => {
                    return acc.concat([
                        {
                            hcpartyId: level.ownerId,
                            extractedKeys: level.extracted.filter((key) => !acc.some((previousLevel) => previousLevel.extractedKeys.includes(key))),
                        },
                    ]);
                }, [])
                    .filter((l) => l.extractedKeys.length > 0)
                    .map(({ hcpartyId, extractedKeys }) => this.findContactIdsByDataOwnerPatientOpeningDate(hcpartyId, extractedKeys, startDate, endDate, descending))).then((results) => [...new Set(results.flat())])
                : Promise.resolve([]));
        });
    }
    /**
     * @deprecated Unused method, bugged: won't work if `hcpartyId` is not the parent hcp and doesn't decrypt contacts.
     * Will be permanently removed in version 8.2.0
     */
    findByPatientSFKs(hcpartyId, patients) {
        return __awaiter(this, void 0, void 0, function* () {
            const perHcpId = {};
            for (const patient of patients) {
                ;
                (yield this.crypto.xapi.secretIdsForHcpHierarchyOf({ entity: patient, type: utils_1.EntityWithDelegationTypeName.Patient }))
                    .reduce((acc, level) => {
                    return acc.concat([
                        {
                            hcpartyId: level.ownerId,
                            extractedKeys: level.extracted.filter((key) => !acc.some((previousLevel) => previousLevel.extractedKeys.includes(key))),
                        },
                    ]);
                }, [])
                    .filter((l) => l.extractedKeys.length > 0)
                    .forEach(({ hcpartyId, extractedKeys }) => {
                    ;
                    (perHcpId[hcpartyId] || (perHcpId[hcpartyId] = [])).push(...extractedKeys);
                });
            }
            return (0, collection_utils_1.uniqBy)((yield Promise.all(Object.keys(perHcpId).map((hcpId) => this.findContactsByHCPartyPatientForeignKeys(hcpartyId, new models.ListOfIds({
                ids: perHcpId[hcpId],
            }))))).flat(), (x) => x.id);
        });
    }
    filterBy(startKey, startDocumentId, limit, body) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    listContactsByOpeningDate(startDate, endDate, hcpartyid, startKey, startDocumentId, limit) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    listServices(body) {
        throw new Error('Cannot call a method that returns services without providing a user for de/encryption');
    }
    findByHCPartyFormId(hcPartyId, formId) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    findByHCPartyFormIds(hcPartyId, body) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    getContact(contactId) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    getContacts(body) {
        throw new Error('Cannot call a method that returns contacts without providing a user for de/encryption');
    }
    modifyContact(body) {
        throw new Error('Cannot call a method that modify contacts without providing a user for de/encryption');
    }
    modifyContacts(body) {
        throw new Error('Cannot call a method that modify contacts without providing a user for de/encryption');
    }
    createContact(body) {
        throw new Error('Cannot call a method that modify contacts without providing a user for de/encryption');
    }
    /**
     * @deprecated use {@link findContactIdsByDataOwnerPatientOpeningDate} instead.
     */
    findByHCPartyPatientSecretFKeys(hcPartyId, secretFKeys, planOfActionIds, skipClosedContacts) {
        return super
            .findByHCPartyPatientSecretFKeys(hcPartyId, secretFKeys, planOfActionIds, skipClosedContacts)
            .then((contacts) => this.decrypt(hcPartyId, contacts));
    }
    /**
     * @deprecated use {@link findContactIdsByDataOwnerPatientOpeningDate} instead.
     */
    findByHCPartyPatientSecretFKeysArray(hcPartyId, secretFKeys, planOfActionIds, skipClosedContacts) {
        return super
            .findByHCPartyPatientSecretFKeysUsingPost(hcPartyId, planOfActionIds, skipClosedContacts, secretFKeys)
            .then((contacts) => this.decrypt(hcPartyId, contacts));
    }
    filterByWithUser(user, startDocumentId, limit, body, collectTiming = false) {
        return super
            .filterContactsBy(startDocumentId, limit, body, collectTiming)
            .then((ctcs) => this.decrypt(user.healthcarePartyId || user.patientId, ctcs.rows).then((decryptedRows) => Object.assign(ctcs, { rows: decryptedRows })));
    }
    filterServicesByWithUser(user, startDocumentId, limit, body, collectTiming = false) {
        return super
            .filterServicesBy(startDocumentId, limit, body, collectTiming)
            .then((svcs) => this.decryptServices(user.healthcarePartyId || user.patientId, svcs.rows).then((decryptedRows) => Object.assign(svcs, { rows: decryptedRows })));
    }
    /**
     * Lists contacts by opening date within a date range and decrypts the results for the given user.
     * @param user the user for decryption
     * @param startDate start of the date range (format: YYYYMMDDHHmmss)
     * @param endDate end of the date range (format: YYYYMMDDHHmmss)
     * @param hcpartyid the healthcare party id
     * @param startKey optional start key for pagination
     * @param startDocumentId optional document id to start pagination from
     * @param limit maximum number of results to return
     * @return a paginated list of decrypted contacts
     */
    listContactsByOpeningDateWithUser(user, startDate, endDate, hcpartyid, startKey, startDocumentId, limit) {
        return super
            .listContactsByOpeningDate(startDate, endDate, hcpartyid, startKey, startDocumentId, limit)
            .then((ctcs) => this.decrypt(user.healthcarePartyId || user.patientId, ctcs.rows).then((decryptedRows) => Object.assign(ctcs, { rows: decryptedRows })));
    }
    /**
     * Gets a service by id and decrypts it for the given user.
     * @param user the user for decryption
     * @param serviceId the service id
     * @return the decrypted service
     */
    getServiceWithUser(user, serviceId) {
        return super
            .getService(serviceId)
            .then((service) => { var _a, _b; return this.decryptServices((_b = (_a = user.healthcarePartyId) !== null && _a !== void 0 ? _a : user.patientId) !== null && _b !== void 0 ? _b : user.deviceId, [service]); })
            .then((decrypted) => decrypted[0]);
    }
    /**
     * Lists services by their ids and decrypts them for the given user.
     * @param user the user for decryption
     * @param serviceIds the list of service ids to retrieve
     * @return an array of decrypted services
     */
    listServicesWithUser(user, serviceIds) {
        var _a;
        return super
            .filterServicesBy(undefined, (_a = serviceIds.ids) === null || _a === void 0 ? void 0 : _a.length, new models_1.FilterChainService({ filter: new ServiceByIdsFilter_1.ServiceByIdsFilter({ ids: serviceIds.ids }) }))
            .then((paginatedList) => { var _a, _b, _c; return this.decryptServices((_b = (_a = user.healthcarePartyId) !== null && _a !== void 0 ? _a : user.patientId) !== null && _b !== void 0 ? _b : user.deviceId, (_c = paginatedList.rows) !== null && _c !== void 0 ? _c : []); });
    }
    /**
     * Finds contacts by healthcare party and form id and decrypts them for the given user.
     * @param user the user for decryption
     * @param hcPartyId the healthcare party id
     * @param formId the form id
     * @return an array of decrypted contacts
     */
    findByHCPartyFormIdWithUser(user, hcPartyId, formId) {
        return super.findByHCPartyFormId(hcPartyId, formId).then((ctcs) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs));
    }
    /**
     * Finds contacts by healthcare party and multiple form ids and decrypts them for the given user.
     * @param user the user for decryption
     * @param hcPartyId the healthcare party id
     * @param body the list of form ids
     * @return an array of decrypted contacts
     */
    findByHCPartyFormIdsWithUser(user, hcPartyId, body) {
        return super.findByHCPartyFormIds(hcPartyId, body).then((ctcs) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs));
    }
    /**
     * Gets a contact by id and decrypts it for the given user.
     * @param user the user for decryption
     * @param contactId the contact id
     * @return the decrypted contact
     */
    getContactWithUser(user, contactId) {
        return super
            .getContact(contactId)
            .then((ctc) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), [ctc]))
            .then((ctcs) => ctcs[0]);
    }
    /**
     * Gets multiple contacts by their ids and decrypts them for the given user.
     * @param user the user for decryption
     * @param body the list of contact ids
     * @return an array of decrypted contacts
     */
    getContactsWithUser(user, body) {
        return super.getContacts(body).then((ctcs) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs));
    }
    /**
     * Modifies an existing contact by encrypting it and saving it to the database.
     * @param user the user for encryption
     * @param body the contact to modify
     * @return the modified and decrypted contact
     */
    modifyContactWithUser(user, body) {
        return __awaiter(this, void 0, void 0, function* () {
            return body ? this.modifyContactAs(this.dataOwnerApi.getDataOwnerIdOf(user), body) : null;
        });
    }
    modifyContactAs(dataOwner, body) {
        return this.encryptAs(dataOwner, [(0, collection_utils_1.cloneDeep)(body)])
            .then((ctcs) => super.modifyContact(ctcs[0]))
            .then((ctc) => this.decrypt(dataOwner, [ctc]))
            .then((ctcs) => ctcs[0]);
    }
    /**
     * Modifies multiple existing contacts by encrypting them and saving them to the database.
     * @param user the user for encryption
     * @param bodies the array of contacts to modify
     * @return an array of modified and decrypted contacts
     */
    modifyContactsWithUser(user, bodies) {
        const _super = Object.create(null, {
            modifyContacts: { get: () => super.modifyContacts }
        });
        return __awaiter(this, void 0, void 0, function* () {
            return bodies
                ? this.encrypt(user, bodies.map((c) => (0, collection_utils_1.cloneDeep)(c)))
                    .then((ctcs) => _super.modifyContacts.call(this, ctcs))
                    .then((ctcs) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs))
                : null;
        });
    }
    /**
     * Creates a new contact by encrypting it and saving it to the database.
     * @param user the user for encryption
     * @param body the contact to create
     * @return the created and decrypted contact
     */
    createContactWithUser(user, body) {
        const _super = Object.create(null, {
            createContact: { get: () => super.createContact }
        });
        return __awaiter(this, void 0, void 0, function* () {
            return body
                ? this.encrypt(user, [(0, collection_utils_1.cloneDeep)(body)])
                    .then((ctcs) => _super.createContact.call(this, ctcs[0]))
                    .then((ctc) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), [ctc]))
                    .then((ctcs) => ctcs[0])
                : null;
        });
    }
    /**
     * Creates multiple new contacts by encrypting them and saving them to the database.
     * @param user the user for encryption
     * @param bodies the array of contacts to create
     * @return an array of created and decrypted contacts
     */
    createContactsWithUser(user, bodies) {
        const _super = Object.create(null, {
            createContacts: { get: () => super.createContacts }
        });
        return __awaiter(this, void 0, void 0, function* () {
            return bodies
                ? this.encrypt(user, bodies.map((c) => (0, collection_utils_1.cloneDeep)(c)))
                    .then((ctcs) => _super.createContacts.call(this, ctcs))
                    .then((ctcs) => this.decrypt(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs))
                : null;
        });
    }
    /**
     * Encrypts the content and custom encrypted fields of services.
     * Services with compound values are recursively encrypted.
     * @param key the encryption key
     * @param rawKey the raw encryption key as a string
     * @param services the array of services to encrypt
     * @return a promise containing the encrypted services
     */
    encryptServices(key, rawKey, services) {
        return Promise.all(services.map((svc) => __awaiter(this, void 0, void 0, function* () {
            if (svc.content &&
                Object.values(svc.content).every((c) => c.compoundValue &&
                    !c.stringValue &&
                    !c.documentId &&
                    !c.measureValue &&
                    !c.medicationValue &&
                    (c.booleanValue === null || c.booleanValue === undefined) &&
                    (c.numberValue === null || c.numberValue === undefined) &&
                    !c.instantValue &&
                    !c.fuzzyDateValue &&
                    !c.binaryValue)) {
                const recursivelyEncryptedContent = Object.fromEntries(yield Promise.all(Object.entries(svc.content).map((p) => __awaiter(this, void 0, void 0, function* () {
                    var _a;
                    if ((_a = p[1].compoundValue) === null || _a === void 0 ? void 0 : _a.length) {
                        return [p[0], Object.assign(Object.assign({}, p[1]), { compoundValue: yield this.encryptServices(key, rawKey, p[1].compoundValue) })];
                    }
                    else
                        return p;
                }))));
                const copyWithEncryptedContent = Object.assign(Object.assign({}, svc), { content: recursivelyEncryptedContent });
                return this.serviceEncryptedFieldsNoContent
                    ? (0, utils_1.encryptObject)(copyWithEncryptedContent, (obj) => {
                        return this.crypto.primitives.AES.encrypt(key, (0, binary_utils_1.utf8_2ua)(JSON.stringify(obj)), rawKey);
                    }, (encryptedSelf) => this.crypto.xapi.tryDecryptJson([{ key, raw: rawKey }], encryptedSelf, false), this.serviceEncryptedFieldsNoContent, 'service')
                    : copyWithEncryptedContent;
            }
            else {
                return (0, utils_1.encryptObject)(svc, (obj) => {
                    return this.crypto.primitives.AES.encrypt(key, (0, binary_utils_1.utf8_2ua)(JSON.stringify(obj)), rawKey);
                }, (encryptedSelf) => this.crypto.xapi.tryDecryptJson([{ key, raw: rawKey }], encryptedSelf, false), this.serviceEncryptedFieldsWithContent, 'service');
            }
        })));
    }
    /**
     * Encrypts contacts for the given user including all services within the contacts.
     * @param user the user for encryption
     * @param ctcs the array of contacts to encrypt
     * @return a promise containing the encrypted contacts
     */
    encrypt(user, ctcs) {
        return this.encryptAs(this.dataOwnerApi.getDataOwnerIdOf(user), ctcs);
    }
    encryptAs(hcpartyId, ctcs) {
        return __awaiter(this, void 0, void 0, function* () {
            var _a;
            const initializedContacts = [];
            for (const ctc of ctcs) {
                initializedContacts.push((_a = (yield this.crypto.xapi.ensureEncryptionKeysInitialised(ctc, utils_1.EntityWithDelegationTypeName.Contact))) !== null && _a !== void 0 ? _a : ctc);
            }
            const allEncrypted = yield this.crypto.xapi.doManyIncrementallyDecryptingKeys(initializedContacts, utils_1.EntityWithDelegationTypeName.Contact, (entity, t, keys) => __awaiter(this, void 0, void 0, function* () {
                for (const k of keys) {
                    try {
                        const encrypted = new models_1.Contact(yield (0, utils_1.encryptObject)(Object.assign(Object.assign({}, entity), { services: entity.services ? yield this.encryptServices(k.key, k.raw, entity.services) : [] }), (obj) => {
                            return this.crypto.primitives.AES.encrypt(k.key, (0, binary_utils_1.utf8_2ua)(JSON.stringify(obj)), k.raw);
                        }, (encryptedSelf) => this.crypto.xapi.tryDecryptJson(keys, encryptedSelf, false), this.contactEncryptedFields, utils_1.EntityWithDelegationTypeName.Contact));
                        return { success: encrypted };
                    }
                    catch (e) {
                        console.warn(`Failed to encrypt contact ${entity.id} with key ${k.raw}`);
                    }
                }
                return null;
            }));
            if (ctcs.length != allEncrypted.size) {
                throw new Error(`Couldn't encrypt contacts ${ctcs.flatMap((e) => (allEncrypted.has(e.id) ? [] : [e.id]))}`);
            }
            return ctcs.map((e) => allEncrypted.get(e.id));
        });
    }
    /**
     * Decrypts contacts and all their encrypted content.
     * @param hcpartyId the healthcare party id (deprecated parameter, kept for compatibility)
     * @param ctcs the array of encrypted contacts
     * @return a promise containing the decrypted contacts
     */
    decrypt(hcpartyId, ctcs) {
        return __awaiter(this, void 0, void 0, function* () {
            return (yield this.crypto.xapi.tryDecryptEntities(ctcs, utils_1.EntityWithDelegationTypeName.Contact, (x) => new models_1.Contact(x))).map(({ entity }) => entity);
        });
    }
    /**
     * Decrypts services and all their encrypted content.
     * @param hcpartyId the healthcare party id (deprecated parameter, kept for compatibility)
     * @param svcs the array of encrypted services
     * @return a promise containing the decrypted services
     */
    decryptServices(hcpartyId, svcs) {
        return __awaiter(this, void 0, void 0, function* () {
            /*TODO
             * not super efficient, re-decrypts the encryption key metadata of services multiple times, but should mostly rely
             * on cached exchange data after the first decryption and should not do any extra requests to the db.
             * We could ensure to use only cached exchange data by doing limited-size batches of services from the same contact
             * id.
             */
            return (yield this.crypto.xapi.tryDecryptEntities(svcs, utils_1.EntityWithDelegationTypeName.Contact, (x) => new models_1.Service(x))).map(({ entity }) => entity);
        });
    }
    /**
     * Finds the contact containing the given service id, prioritizing the latest version by valueDate.
     * @param ctcs the array of contacts to search
     * @param svcId the service id to find
     * @return the contact containing the service or undefined if not found
     */
    contactOfService(ctcs, svcId) {
        let latestContact = undefined;
        let latestService;
        ctcs.forEach((c) => {
            const s = c.services.find((it) => svcId === it.id);
            if (s && (!latestService || (0, date_fns_1.isAfter)((0, date_fns_1.parse)('' + s.valueDate, 'yyyyMMddHHmmss', new Date()), (0, date_fns_1.parse)('' + latestService.valueDate, 'yyyyMMddHHmmss', new Date())))) {
                latestContact = c;
                latestService = s;
            }
        });
        return latestContact;
    }
    /**
     * Extracts and filters services from contacts, keeping only the latest version of each service and excluding deleted services.
     * @param ctcs the array of contacts containing services
     * @param filter a predicate function to filter services: (service, contact) => boolean
     * @return an array of filtered services with contactId set
     */
    filteredServices(ctcs, filter) {
        const byIds = {};
        ctcs.forEach((c) => (c.services || [])
            .filter((s) => filter(s, c))
            .forEach((s) => {
            const ps = byIds[s.id];
            if (!ps || !ps.modified || (s.modified && ps.modified < s.modified)) {
                byIds[s.id] = s;
                s.contactId = c.id;
            }
        }));
        return Object.values(byIds).filter((s) => !s.deleted && !s.endOfLife);
    }
    /**
     * Extracts and filters services from contacts, returning a promise. Same as filteredServices but wrapped in a promise.
     * @param ctcs the array of contacts containing services
     * @param filter a predicate function to filter services: (service, contact) => boolean
     * @return a promise containing an array of filtered services
     */
    filterServices(ctcs, filter) {
        return Promise.resolve(this.filteredServices(ctcs, filter));
    }
    /**
     * Gets all services from a contact that have the specified label.
     * @param ctc the contact
     * @param label the service label to filter by
     * @return an array of services with the given label
     */
    services(ctc, label) {
        return ctc.services.filter((s) => s.label === label);
    }
    /**
     * Gets the preferred content for a service based on language preference, falling back to French then first available language.
     * @param svc the service
     * @param lng the preferred language code
     * @return the content in the preferred language or null if no content exists
     */
    preferredContent(svc, lng) {
        return (svc && svc.content && (svc.content[lng] || svc.content['fr'] || (Object.keys(svc.content)[0] ? svc.content[Object.keys(svc.content)[0]] : null)));
    }
    /**
     * Extracts the value from content, prioritizing string, number, measure, medication, and boolean values in that order.
     * @param c the content
     * @return the extracted value or undefined if no value is present
     */
    contentValue(c) {
        return (c.stringValue ||
            ((c.numberValue || c.numberValue === 0) && c.numberValue) ||
            (c.measureValue && (c.measureValue.value || c.measureValue.value === 0) ? c.measureValue : null) ||
            c.medicationValue ||
            c.booleanValue);
    }
    /**
     * Gets a short human-readable description of a service in the preferred language.
     * @param svc the service
     * @param lng the preferred language code
     * @return a short description of the service content or empty string
     */
    shortServiceDescription(svc, lng) {
        const c = this.preferredContent(svc, lng);
        return !c ? '' : this.shortContentDescription(c, lng, svc.label);
    }
    /**
     * Gets a short human-readable description of content, formatted appropriately for the content type.
     * @param c the content
     * @param lng the language code for medication formatting
     * @param label optional label used for boolean content display
     * @return a short description of the content
     */
    shortContentDescription(c, lng, label) {
        return (c.stringValue ||
            ((c.numberValue || c.numberValue === 0) && c.numberValue) ||
            (c.measureValue &&
                '' +
                    (c.measureValue.value || c.measureValue.value === 0 ? c.measureValue.value : '-') +
                    (c.measureValue.unit ? ' ' + c.measureValue.unit : '')) ||
            (c.medicationValue ? this.medication().medicationToString(c.medicationValue, lng) : null) ||
            (c.booleanValue && label) ||
            'OK');
    }
    /**
     * Extracts the medication value from a service's content in the preferred language.
     * @param svc the service
     * @param lng the preferred language code
     * @return the medication value or undefined if not present
     */
    medicationValue(svc, lng) {
        const c = svc && svc.content && (svc.content[lng] || svc.content['fr'] || (Object.keys(svc.content)[0] ? svc.content[Object.keys(svc.content)[0]] : null));
        return c && c.medicationValue;
    }
    /**
     * Checks if content has any data value set (string, number, measure, boolean, medication, or document).
     * @param c the content to check
     * @return true if content has data, false otherwise
     */
    contentHasData(c) {
        return c.stringValue || c.numberValue || c.measureValue || c.booleanValue || c.booleanValue === false || c.medicationValue || c.documentId;
    }
    /**
     * Localizes a value by selecting the appropriate language from an object, falling back through FR, EN, and NL.
     * @param e the object with language-keyed values or a simple value
     * @param lng the preferred language code
     * @return the localized value or null if e is null/undefined
     */
    localize(e, lng) {
        if (!e) {
            return null;
        }
        return e[lng] || e.fr || e.FR || e.en || e.EN || e.nl || e.NL;
    }
    /**
     * Modifies the subcontacts this svc belongs to while minimizing the number of references to the svcs inside the subcontacts
     * After the invocation, there is at least one subcontact with provided poaId and heId that contains the svc
     * The svc is not removed from a previous subcontact it would belong to except if the new conditions are compatible
     * Note that undefined and null do not have the same meaning for formId
     * If formId is null: the subcontact which refers svc must have a null formId
     * If formId is undefined, the subcontact can have any value for formId
     *
     * When a svc does not exist yet in the current contact but exists in a previous contact, all the scs it was belonging to are
     * copied in the current contact
     *
     * the svc returned is the one that's inside the ctc
     *
     * @param ctc
     * @param user
     * @param ctcs
     * @param svc
     * @param formId
     * @param poaId aMap {heId2: [poaId11, poaId12], heId2: [poaId21] }
     * @param heId an Array of heIds, equivalent to poaIds = {heId: [], ...}
     * @param init
     * @returns {*}
     */
    promoteServiceInContact(ctc, user, ctcs, svc, formId, poaIds, heIds, init) {
        if (!ctc) {
            return null;
        }
        const existing = ctc.services.find((s) => s.id === svc.id);
        const promoted = Object.assign(existing || {}, svc, {
            author: user.id,
            responsible: this.dataOwnerApi.getDataOwnerIdOf(user),
            modified: new Date().getTime(),
        });
        if (!existing) {
            ;
            (ctc.services || (ctc.services = [])).push(promoted);
        }
        const allSubcontactsInCurrentContactContainingService = (ctc.subContacts || []).filter((csc) => (csc.services || []).some((s) => s.serviceId === svc.id));
        //Rearrange poaIds and heIds as a hierarchy
        const hierarchyOfHeAndPoaIds = {};
        (heIds || []).forEach((id) => (hierarchyOfHeAndPoaIds[id || '_'] = []));
        Object.keys(poaIds || {}).forEach((k) => {
            const poas = hierarchyOfHeAndPoaIds[k];
            if (poas) {
                hierarchyOfHeAndPoaIds[k] = [...poas, ...(poaIds || {})[k]];
            }
            else {
                hierarchyOfHeAndPoaIds[k] = (poaIds || {})[k];
            }
        });
        const pastCtc = (svc.contactId && svc.contactId !== ctc.id && ctcs.find((c) => c.id === svc.contactId)) ||
            ctcs.reduce((selected, c) => {
                const candidate = (c.services || []).find((s) => s.id === svc.id);
                return ctc.id !== c.id && candidate && (selected.s === null || (0, utils_1.before)(selected.s.modified || 0, candidate.modified || 0))
                    ? { s: candidate, c: c }
                    : selected;
            }, { s: null, c: null }).c;
        //Make sure that all scs the svc was belonging to are copied inside the current contact
        pastCtc &&
            pastCtc
                .subContacts.filter((psc) => psc.services.some((s) => s.serviceId === svc.id))
                .forEach((psc) => {
                const sameInCurrent = allSubcontactsInCurrentContactContainingService.find((csc) => csc.formId === psc.formId && csc.planOfActionId === psc.planOfActionId && csc.healthElementId === psc.healthElementId);
                if (sameInCurrent) {
                    if (!sameInCurrent.services.some((s) => s.serviceId === svc.id)) {
                        sameInCurrent.services.push({ serviceId: svc.id });
                    }
                }
                else {
                    const newSubContact = Object.assign(Object.assign({}, psc), { services: [{ serviceId: svc.id }] });
                    ctc.subContacts.push(newSubContact);
                    allSubcontactsInCurrentContactContainingService.push(newSubContact);
                }
            });
        if (!Object.keys(hierarchyOfHeAndPoaIds).length) {
            hierarchyOfHeAndPoaIds._ = []; //Default is to have at least one option with heId equals to null (represented by _)
        }
        Object.keys(hierarchyOfHeAndPoaIds).forEach((heId) => {
            if (heId === '_') {
                heId = null;
            }
            const subPoaIds = heId ? hierarchyOfHeAndPoaIds[heId] : [];
            ((subPoaIds || []).length ? subPoaIds : [null]).forEach((poaId) => {
                //Create or assign subcontacts for all pairs he/poa (can be null/null)
                let destinationSubcontact = ctc.subContacts.find((sc) => (!formId || sc.formId === formId) &&
                    ((!poaId && !sc.planOfActionId) || sc.planOfActionId === poaId) &&
                    ((!heId && !sc.healthElementId) || sc.healthElementId === heId));
                if (!destinationSubcontact) {
                    ctc.subContacts.push((destinationSubcontact = new models.SubContact({
                        formId: formId || undefined,
                        planOfActionId: poaId,
                        healthElementId: heId,
                        services: [],
                    })));
                }
                const redundantSubcontact = allSubcontactsInCurrentContactContainingService.find((aSc) => destinationSubcontact === aSc) ||
                    allSubcontactsInCurrentContactContainingService.find((aSc) => (!aSc.planOfActionId || aSc.planOfActionId === destinationSubcontact.planOfActionId) &&
                        (!aSc.healthElementId || aSc.healthElementId === destinationSubcontact.healthElementId) &&
                        (!aSc.formId || aSc.formId === destinationSubcontact.formId)); // Find a compatible sc: one that does not contain extra and ≠ information than the destination
                if (redundantSubcontact && redundantSubcontact !== destinationSubcontact) {
                    redundantSubcontact.services.splice(redundantSubcontact.services.findIndex((link) => link.serviceId === svc.id), 1);
                }
                if (!destinationSubcontact.services.some((s) => s.serviceId === svc.id)) {
                    destinationSubcontact.services.push({ serviceId: svc.id });
                }
            });
        });
        return (init && init(promoted)) || promoted;
    }
    /**
     * Checks if a service has numeric content (measure or number value) in the preferred language.
     * @param svc the service
     * @param lng the preferred language code
     * @return true if the service has numeric content, false otherwise
     */
    isNumeric(svc, lng) {
        const c = this.preferredContent(svc, lng);
        return c && (c.measureValue || c.numberValue || c.numberValue == 0);
    }
    /**
     * Returns utility functions for working with services.
     * @return an object with helper methods for creating and manipulating services
     */
    service() {
        return {
            newInstance: (user, s) => {
                var _a, _b;
                return (Object.assign({ id: this.crypto.primitives.randomUuid(), _type: 'org.taktik.icure.entities.embed.Service', created: new Date().getTime(), modified: new Date().getTime(), responsible: (_a = s === null || s === void 0 ? void 0 : s.responsible) !== null && _a !== void 0 ? _a : (this.autofillAuthor ? this.dataOwnerApi.getDataOwnerIdOf(user) : undefined), author: (_b = s === null || s === void 0 ? void 0 : s.author) !== null && _b !== void 0 ? _b : (this.autofillAuthor ? user.id : undefined), codes: [], tags: [], content: {}, valueDate: parseInt((0, date_fns_1.format)(new Date(), 'yyyyMMddHHmmss')) }, s));
            },
        };
    }
    /**
     * Returns utility functions for working with medication content.
     * @return an object with helper methods for formatting and displaying medication information
     */
    medication() {
        const regimenScores = {
            afterwakingup: 63000,
            beforebreakfast: 70000,
            duringbreakfast: 80000,
            afterbreakfast: 90000,
            morning: 100000,
            betweenbreakfastandlunch: 103000,
            beforelunch: 113000,
            midday: 120000,
            duringlunch: 123000,
            afterlunch: 130000,
            afternoon: 140000,
            betweenlunchanddinner: 160000,
            beforedinner: 180000,
            duringdinner: 190000,
            afterdinner: 200000,
            evening: 210000,
            betweendinnerandsleep: 213000,
            thehourofsleep: 220000,
            night: 230000,
            beforemeals: -30000,
            betweenmeals: -20000,
            aftermeals: -10000,
        };
        const myself = {
            regimenScores: function () {
                return regimenScores;
            },
            medicationNameToString: function (m) {
                return m && m.compoundPrescription
                    ? m.compoundPrescription
                    : m && m.substanceProduct
                        ? myself.productToString(m && m.substanceProduct)
                        : myself.productToString(m && m.medicinalProduct);
            },
            reimbursementReasonToString: (m, lang) => {
                return m && m.reimbursementReason && m.reimbursementReason.label && m.reimbursementReason.label.hasOwnProperty(lang)
                    ? m.reimbursementReason.label[lang]
                    : '';
            },
            medicationToString: (m, lang) => {
                let res = `${myself.medicationNameToString(m)}, ${myself.posologyToString(m, lang)}`;
                const reason = myself.reimbursementReasonToString(m, lang);
                res = m.numberOfPackages
                    ? `${m.numberOfPackages} ${m.numberOfPackages > 1 ? this.i18n[lang].packagesOf : this.i18n[lang].packageOf} ${res}`
                    : res;
                res = m.duration ? `${res} ${this.i18n[lang].during} ${myself.durationToString(m.duration, lang)}` : res;
                res = reason ? `${res} (${reason})` : res;
                return res;
            },
            productToString: (m) => {
                if (!m) {
                    return '';
                }
                return m.intendedname;
            },
            posologyToString: (m, lang) => {
                if (m) {
                    if (m.instructionForPatient && m.instructionForPatient.length) {
                        return m.instructionForPatient;
                    }
                    if (!m.regimen || !m.regimen.length) {
                        return '';
                    }
                    const unit = m.regimen[0].administratedQuantity && m.regimen[0].administratedQuantity.administrationUnit
                        ? m.regimen[0].administratedQuantity.administrationUnit.code
                        : m.regimen[0].administratedQuantity && m.regimen[0].administratedQuantity.unit;
                    let quantity = m.regimen[0].administratedQuantity && m.regimen[0].administratedQuantity.quantity;
                    m.regimen.slice(1).find((ri) => {
                        const oUnit = ri.administratedQuantity && ri.administratedQuantity.administrationUnit
                            ? ri.administratedQuantity.administrationUnit.code
                            : ri.administratedQuantity && ri.administratedQuantity.unit;
                        const oQuantity = ri.administratedQuantity && ri.administratedQuantity.quantity;
                        if (oQuantity !== quantity) {
                            quantity = -1;
                        }
                        return oUnit !== unit && oQuantity !== quantity;
                    });
                    const cplxRegimen = !unit || quantity < 0;
                    const quantityUnit = cplxRegimen ? `1 ${this.i18n[lang].take_s_}` : `${quantity} ${unit || this.i18n[lang].take_s_}`;
                    const dayPeriod = m.regimen.find((r) => r.weekday !== null && r.weekday !== undefined)
                        ? this.i18n[lang].weekly
                        : m.regimen.find((r) => r.date)
                            ? this.i18n[lang].monthly
                            : this.i18n[lang].daily;
                    return `${quantityUnit}, ${m.regimen.length} x ${dayPeriod}, ${(0, collection_utils_1.sortBy)(m.regimen, (r) => (r.date ? r.date * 1000000 : 29990000000000) +
                        (r.dayNumber || 0) * 1000000 +
                        ((r.weekday && r.weekday.weekNumber) || 0) * 7 * 1000000 +
                        (r.timeOfDay ? r.timeOfDay : r.dayPeriod && r.dayPeriod.code ? regimenScores[r.dayPeriod.code] : 0))
                        .map((r) => (cplxRegimen ? myself.regimenToExtString(r, lang) : myself.regimenToString(r, lang)))
                        .join(', ')}`;
                }
            },
            frequencyToString: (m, lang) => {
                if (m.instructionForPatient && m.instructionForPatient.length) {
                    return m.instructionForPatient;
                }
                if (!m.regimen || !m.regimen.length) {
                    return '';
                }
                const dayPeriod = m.regimen.find((r) => r.weekday !== null && r.weekday !== undefined)
                    ? this.i18n[lang].weekly
                    : m.regimen.find((r) => r.date)
                        ? this.i18n[lang].monthly
                        : this.i18n[lang].daily;
                return `${m.regimen.length} x ${dayPeriod}`;
            },
            durationToString: (d, lang) => {
                return d.value ? `${d.value} ${this.localize(d.unit.label, lang)}` : '';
            },
            regimenToExtString: (r, lang) => {
                const desc = myself.regimenToString(r, lang);
                return ((r.administratedQuantity && r.administratedQuantity.quantity && desc
                    ? `${desc} (${r.administratedQuantity.quantity} ${(r.administratedQuantity.administrationUnit ? r.administratedQuantity.administrationUnit.code : r.administratedQuantity.unit) ||
                        this.i18n[lang].take_s_})`
                    : desc) || '');
            },
            regimenToString: (r, lang) => {
                let res = r.date
                    ? `${this.i18n[lang].the} ${(0, date_fns_1.format)(r.date, 'dd/MM/yyyy')}`
                    : r.dayNumber
                        ? `${this.i18n[lang].onDay} ${r.dayNumber}`
                        : r.weekday && r.weekday.weekday
                            ? `${this.i18n[lang].on} ${r.weekday.weekday}`
                            : null;
                if (r.dayPeriod && r.dayPeriod.code && r.dayPeriod.code.length) {
                    res = res
                        ? `${res} ${this.i18n[lang][r.dayPeriod.code] || this.localize(r.dayPeriod.label, lang) || r.dayPeriod.code}`
                        : this.i18n[lang][r.dayPeriod.code] || this.localize(r.dayPeriod.label, lang) || r.dayPeriod.code;
                }
                if (r.timeOfDay) {
                    const timeOfDay = r.timeOfDay === 120000
                        ? this.i18n[lang].noon
                        : `${Math.floor(r.timeOfDay / 10000)}:${('' + (Math.floor(r.timeOfDay / 100) % 100)).replace(/^(.)$/, '0$1')}`;
                    res = res ? res + ' ' + this.i18n[lang].at + ' ' + timeOfDay : timeOfDay;
                }
                return res;
            },
            localize: (s, lang) => {
                if (!s) {
                    return s;
                }
                return (this.i18n[lang][s] ||
                    (this.i18n[lang][s.toLowerCase()] &&
                        this.i18n[lang][s.toLowerCase()]
                            .split('')
                            .map((c, idx) => (idx >= s.length || s[idx].toLocaleLowerCase() === s[idx] ? c : c.toLocaleUpperCase()))
                            .join('')) ||
                    s); //Applies the (lower/upper)case to the translated lowercase version of the input string (s)
            },
        };
        return myself;
    }
    /**
     * @param contact a contact
     * @return the id of the patient that the contact refers to, retrieved from the encrypted metadata. Normally there should only be one element
     * in the returned array, but in case of entity merges there could be multiple values.
     */
    decryptPatientIdOf(contact) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.crypto.xapi.owningEntityIdsOf({ entity: contact, type: utils_1.EntityWithDelegationTypeName.Contact }, undefined);
        });
    }
    /**
     * @param service a service
     * @return the id of the patient that the service refers to, retrieved from the encrypted metadata. Normally there should only be one element
     * in the returned array, but in case of entity merges there could be multiple values.
     */
    decryptPatientIdOfService(service) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.crypto.xapi.owningEntityIdsOf({ entity: service, type: utils_1.EntityWithDelegationTypeName.Contact }, undefined);
        });
    }
    /**
     * @return if the logged data owner has write access to the content of the given contact
     */
    hasWriteAccess(contact) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.crypto.xapi.hasWriteAccess({ entity: contact, type: utils_1.EntityWithDelegationTypeName.Contact });
        });
    }
    /**
     * Share an existing contact with other data owners, allowing them to access the non-encrypted data of the contact and optionally also
     * the encrypted content, with read-only or read-write permissions.
     * @param delegateId the id of the data owner which will be granted access to the contact.
     * @param contact the contact to share.
     * @param options optional parameters to customize the sharing behaviour:
     * - shareSecretIds: specifies which secret ids of the entity should be shared. If not provided all secret ids available to the current user will be shared
     * - shareEncryptionKey: specifies if the encryption key of the access log should be shared with the delegate, giving access to all encrypted
     * content of the entity, excluding other encrypted metadata (defaults to {@link ShareMetadataBehaviour.IF_AVAILABLE}). Note that by default a
     * contact does not have encrypted content.
     * - sharePatientId: specifies if the id of the patient that this contact refers to should be shared with the delegate (defaults to
     * {@link ShareMetadataBehaviour.IF_AVAILABLE}).
     * - requestedPermissions: the requested permissions for the delegate, defaults to {@link RequestedPermissionEnum.MAX_WRITE}.
     * @return the updated entity
     */
    shareWith(delegateId_1, contact_1) {
        return __awaiter(this, arguments, void 0, function* (delegateId, contact, options = {}) {
            return this.shareWithMany(contact, { [delegateId]: options });
        });
    }
    /**
     * Share an existing contact with other data owners, allowing them to access the non-encrypted data of the contact and optionally also
     * the encrypted content, with read-only or read-write permissions.
     * @param contact the contact to share.
     * @param delegates associates the id of data owners which will be granted access to the entity, to the following sharing options:
     * - shareSecretIds: specifies which secret ids of the entity should be shared. If not provided all secret ids available to the current user will be shared
     * - shareEncryptionKey: specifies if the encryption key of the access log should be shared with the delegate, giving access to all encrypted
     * content of the entity, excluding other encrypted metadata (defaults to {@link ShareMetadataBehaviour.IF_AVAILABLE}). Note that by default a
     * contact does not have encrypted content.
     * - sharePatientId: specifies if the id of the patient that this contact refers to should be shared with the delegate (defaults to
     * {@link ShareMetadataBehaviour.IF_AVAILABLE}).
     * - requestedPermissions: the requested permissions for the delegate, defaults to {@link RequestedPermissionEnum.MAX_WRITE}.
     * @return the updated entity
     */
    shareWithMany(contact, delegates) {
        return __awaiter(this, void 0, void 0, function* () {
            return (yield this.tryShareWithMany(contact, delegates)).updatedEntityOrThrow;
        });
    }
    /**
     * Share an existing contact with other data owners, allowing them to access the non-encrypted data of the contact and optionally also
     * the encrypted content, with read-only or read-write permissions.
     * @param contact the contact to share.
     * @param delegates associates the id of data owners which will be granted access to the entity, to the following sharing options:
     * - shareSecretIds: specifies which secret ids of the entity should be shared. If not provided all secret ids available to the current user will be shared
     * - shareEncryptionKey: specifies if the encryption key of the access log should be shared with the delegate, giving access to all encrypted
     * content of the entity, excluding other encrypted metadata (defaults to {@link ShareMetadataBehaviour.IF_AVAILABLE}). Note that by default a
     * contact does not have encrypted content.
     * - sharePatientId: specifies if the id of the patient that this contact refers to should be shared with the delegate (defaults to
     * {@link ShareMetadataBehaviour.IF_AVAILABLE}).
     * - requestedPermissions: the requested permissions for the delegate, defaults to {@link RequestedPermissionEnum.MAX_WRITE}.
     * @return a promise which will contain the result of the operation: the updated entity if the operation was successful or details of the error if
     * the operation failed.
     */
    tryShareWithMany(contact, delegates) {
        return __awaiter(this, void 0, void 0, function* () {
            const self = yield this.dataOwnerApi.getCurrentDataOwnerId();
            // All entities should have an encryption key.
            const entityWithEncryptionKey = yield this.crypto.xapi.ensureEncryptionKeysInitialised(contact, utils_1.EntityWithDelegationTypeName.Contact);
            const updatedEntity = entityWithEncryptionKey ? yield this.modifyContactAs(self, entityWithEncryptionKey) : contact;
            return this.crypto.xapi
                .simpleShareOrUpdateEncryptedEntityMetadata({
                entity: updatedEntity,
                type: utils_1.EntityWithDelegationTypeName.Contact,
            }, Object.fromEntries(Object.entries(delegates).map(([delegateId, options]) => [
                delegateId,
                {
                    requestedPermissions: options.requestedPermissions,
                    shareEncryptionKeys: options.shareEncryptionKey,
                    shareOwningEntityIds: options.sharePatientId,
                    shareSecretIds: options.shareSecretIds,
                },
            ])), (x) => this.bulkShareContacts(x))
                .then((r) => r.mapSuccessAsync((e) => this.decrypt(self, [e]).then((es) => es[0])));
        });
    }
    /**
     * Gets the data owners that have access to the contact and their permission levels.
     * @param entity the contact
     * @return a promise containing the permissions by data owner id and whether there are unknown anonymous data owners
     */
    getDataOwnersWithAccessTo(entity) {
        return this.crypto.delegationsDeAnonymization.getDataOwnersWithAccessTo({ entity, type: utils_1.EntityWithDelegationTypeName.Contact });
    }
    /**
     * Gets all encryption keys available for the contact.
     * @param entity the contact
     * @return a promise containing an array of encryption key hex strings
     */
    getEncryptionKeysOf(entity) {
        return this.crypto.xapi.encryptionKeysOf({ entity, type: utils_1.EntityWithDelegationTypeName.Contact }, undefined);
    }
    /**
     * Subscribes to service events (create, update, delete) and receives decrypted services.
     * @param eventTypes the types of events to subscribe to
     * @param filter optional filter to apply to services
     * @param eventFired callback function invoked when an event occurs, receives the decrypted service
     * @param options optional subscription options
     * @return a connection that can be closed to unsubscribe
     */
    subscribeToServiceEvents(eventTypes_1, filter_1, eventFired_1) {
        return __awaiter(this, arguments, void 0, function* (eventTypes, filter, eventFired, options = {}) {
            const currentUser = yield this.userApi.getCurrentUser();
            return (0, utils_1.subscribeToEntityEvents)(this.host, this.authApi, 'Service', eventTypes, filter, eventFired, options, (encrypted) => __awaiter(this, void 0, void 0, function* () { return (yield this.decryptServices(currentUser.healthcarePartyId, [encrypted]))[0]; })).then((ws) => new Connection_1.ConnectionImpl(ws));
        });
    }
    /**
     * Subscribes to contact events (create, update, delete) and receives decrypted contacts.
     * @param eventTypes the types of events to subscribe to
     * @param filter optional filter to apply to contacts
     * @param eventFired callback function invoked when an event occurs, receives the decrypted contact
     * @param options optional subscription options
     * @return a connection that can be closed to unsubscribe
     */
    subscribeToContactEvents(eventTypes_1, filter_1, eventFired_1) {
        return __awaiter(this, arguments, void 0, function* (eventTypes, filter, eventFired, options = {}) {
            const currentUser = yield this.userApi.getCurrentUser();
            return (0, utils_1.subscribeToEntityEvents)(this.host, this.authApi, utils_1.EntityWithDelegationTypeName.Contact, eventTypes, filter, eventFired, options, (encrypted) => __awaiter(this, void 0, void 0, function* () { return (yield this.decrypt(currentUser.healthcarePartyId, [encrypted]))[0]; })).then((ws) => new Connection_1.ConnectionImpl(ws));
        });
    }
    /**
     * Creates or updates de-anonymization metadata for delegations in a contact.
     * @param entity the contact
     * @param delegates the array of delegate ids to create de-anonymization metadata for
     * @return a promise that completes when the metadata is created/updated
     */
    createDelegationDeAnonymizationMetadata(entity, delegates) {
        return this.crypto.delegationsDeAnonymization.createOrUpdateDeAnonymizationInfo({ entity, type: utils_1.EntityWithDelegationTypeName.Contact }, delegates);
    }
}
exports.IccContactXApi = IccContactXApi;
//# sourceMappingURL=icc-contact-x-api.js.map