import { FAILURE_MESSAGE, METHOD_START_MESSAGE, SCIM_ENTERPRISE_USER, SCIM_WEBEXIDENTITY_USER, STATUS_CODE, SUCCESS_MESSAGE, } from '../common/constants';
import { HTTP_METHODS } from '../common/types';
import SDKConnector from '../SDKConnector';
import log from '../Logger';
import { CONTACTS_CLIENT, CONTACTS_SCHEMA, CONTACT_FILTER, DEFAULT_GROUP_NAME, ENCRYPT_FILTER, GROUP_FILTER, METHODS, OR, SCIM_ID_FILTER, USERS, encryptedFields, } from './constants';
import { ContactType, GroupType, } from './types';
import { scimQuery, serviceErrorCodeHandler, uploadLogs } from '../common/Utils';
export class ContactsClient {
    sdkConnector;
    encryptionKeyUrl;
    webex;
    groups;
    contacts;
    defaultGroupId;
    contactsServiceUrl;
    constructor(webex, logger) {
        this.sdkConnector = SDKConnector;
        if (!this.sdkConnector.getWebex()) {
            SDKConnector.setWebex(webex);
        }
        this.webex = this.sdkConnector.getWebex();
        this.encryptionKeyUrl = '';
        this.groups = undefined;
        this.contacts = undefined;
        this.defaultGroupId = '';
        this.contactsServiceUrl =
            this.webex.internal.services._serviceUrls?.contactsService ||
                this.webex.internal.services.get(this.webex.internal.services._activeServices.contactsService);
        log.setLogger(logger.level, CONTACTS_CLIENT);
    }
    async decryptContactDetail(encryptionKeyUrl, contactDetails) {
        const decryptedContactDetail = [...contactDetails];
        const decryptedValues = await Promise.all(decryptedContactDetail.map((detail) => this.webex.internal.encryption.decryptText(encryptionKeyUrl, detail.value)));
        decryptedValues.forEach((decryptedValue, index) => {
            decryptedContactDetail[index].value = decryptedValue;
        });
        return decryptedContactDetail;
    }
    async encryptContactDetail(encryptionKeyUrl, contactDetails) {
        const encryptedContactDetail = [...contactDetails];
        const encryptedValues = await Promise.all(encryptedContactDetail.map((detail) => this.webex.internal.encryption.encryptText(encryptionKeyUrl, detail.value)));
        encryptedValues.forEach((encryptedValue, index) => {
            encryptedContactDetail[index].value = encryptedValue;
        });
        return encryptedContactDetail;
    }
    async encryptContact(contact) {
        const { encryptionKeyUrl } = contact;
        const encryptedContact = { ...contact };
        const encryptionPromises = Object.values(encryptedFields).map(async (field) => {
            switch (field) {
                case encryptedFields.ADDRESS_INFO: {
                    const plaintextAddressInfo = encryptedContact.addressInfo;
                    let encryptedAddressInfo;
                    if (plaintextAddressInfo) {
                        const encryptedAddressInfoPromises = Object.entries(plaintextAddressInfo).map(async ([key, value]) => [
                            key,
                            await this.webex.internal.encryption.encryptText(encryptionKeyUrl, value),
                        ]);
                        encryptedAddressInfo = Object.fromEntries(await Promise.all(encryptedAddressInfoPromises));
                    }
                    return [field, encryptedAddressInfo];
                }
                case encryptedFields.EMAILS:
                case encryptedFields.PHONE_NUMBERS:
                case encryptedFields.SIP_ADDRESSES: {
                    const plainTextDetails = encryptedContact[field];
                    let encryptedDetails;
                    if (plainTextDetails) {
                        encryptedDetails = await this.encryptContactDetail(encryptionKeyUrl, plainTextDetails);
                    }
                    return [field, encryptedDetails];
                }
                default: {
                    let encryptedValue;
                    if (Object.values(encryptedFields).includes(field) && encryptedContact[field]) {
                        encryptedValue = await this.webex.internal.encryption.encryptText(encryptionKeyUrl, encryptedContact[field]);
                    }
                    return [field, encryptedValue];
                }
            }
        });
        const encryptedFieldsList = await Promise.all(encryptionPromises);
        encryptedFieldsList.forEach(([field, value]) => {
            if (value !== undefined) {
                encryptedContact[field] = value;
            }
        });
        return encryptedContact;
    }
    async decryptContact(contact) {
        const { encryptionKeyUrl } = contact;
        const decryptedContact = { ...contact };
        const decryptionPromises = Object.values(encryptedFields).map(async (field) => {
            switch (field) {
                case encryptedFields.ADDRESS_INFO: {
                    const plaintextAddressInfo = decryptedContact.addressInfo;
                    let decryptedAddressInfo;
                    if (plaintextAddressInfo) {
                        const decryptedAddressInfoPromises = Object.entries(plaintextAddressInfo).map(async ([key, value]) => [
                            key,
                            await this.webex.internal.encryption.decryptText(encryptionKeyUrl, value),
                        ]);
                        decryptedAddressInfo = Object.fromEntries(await Promise.all(decryptedAddressInfoPromises));
                    }
                    return [field, decryptedAddressInfo];
                }
                case encryptedFields.EMAILS:
                case encryptedFields.PHONE_NUMBERS:
                case encryptedFields.SIP_ADDRESSES: {
                    const plainTextDetails = decryptedContact[field];
                    let decryptedDetails;
                    if (plainTextDetails) {
                        decryptedDetails = await this.decryptContactDetail(encryptionKeyUrl, plainTextDetails);
                    }
                    return [field, decryptedDetails];
                }
                default: {
                    let decryptedValue;
                    if (Object.values(encryptedFields).includes(field) && decryptedContact[field]) {
                        decryptedValue = await this.webex.internal.encryption.decryptText(encryptionKeyUrl, decryptedContact[field]);
                    }
                    return [field, decryptedValue];
                }
            }
        });
        const decryptedFieldsList = await Promise.all(decryptionPromises);
        decryptedFieldsList.forEach(([field, value]) => {
            if (value !== undefined) {
                decryptedContact[field] = value;
            }
        });
        return decryptedContact;
    }
    resolveCloudContacts(contactsDataMap, inputList) {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: 'resolveCloudContacts',
        };
        const finalContactList = [];
        const resolvedList = [];
        try {
            inputList.Resources.forEach((item) => {
                resolvedList.push(item.id);
            });
            Object.values(contactsDataMap).forEach((item) => {
                const isResolved = resolvedList.some((listItem) => listItem === item.contactId);
                if (!isResolved) {
                    finalContactList.push({ ...item, resolved: false });
                }
            });
            for (let n = 0; n < inputList.Resources.length; n += 1) {
                const filteredContact = inputList.Resources[n];
                const { displayName, emails, phoneNumbers, photos } = filteredContact;
                let sipAddresses;
                if (filteredContact[SCIM_WEBEXIDENTITY_USER]) {
                    sipAddresses = filteredContact[SCIM_WEBEXIDENTITY_USER].sipAddresses;
                }
                const firstName = filteredContact.name?.givenName;
                const lastName = filteredContact.name?.familyName;
                const manager = filteredContact[SCIM_ENTERPRISE_USER]?.manager?.displayName;
                const department = filteredContact[SCIM_ENTERPRISE_USER]?.department;
                const avatarURL = photos?.length ? photos[0].value : '';
                const { contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups } = contactsDataMap[inputList.Resources[n].id];
                const cloudContact = {
                    avatarUrlDomain,
                    avatarURL,
                    contactId: inputList.Resources[n].id,
                    contactType,
                    department,
                    displayName,
                    emails,
                    encryptionKeyUrl,
                    firstName,
                    groups,
                    lastName,
                    manager,
                    ownerId,
                    phoneNumbers,
                    sipAddresses,
                    resolved: true,
                };
                finalContactList.push(cloudContact);
            }
        }
        catch (error) {
            log.warn('Error occurred while parsing resolved contacts', loggerContext);
            return null;
        }
        return finalContactList;
    }
    async getContacts() {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.GET_CONTACTS,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        const contactList = [];
        const cloudContactsMap = {};
        try {
            const response = await this.webex.request({
                uri: `${this.contactsServiceUrl}/${ENCRYPT_FILTER}/${USERS}/${CONTACT_FILTER}`,
                method: HTTP_METHODS.GET,
            });
            log.log(`Response code: ${response.statusCode} and Response trackingId: ${response?.headers?.trackingid}`, loggerContext);
            const responseBody = response.body;
            if (!responseBody) {
                throw new Error(`${response}`);
            }
            const { contacts, groups } = responseBody;
            await Promise.all(contacts.map(async (contact) => {
                if (contact.contactType === ContactType.CUSTOM) {
                    const decryptedContact = await this.decryptContact(contact);
                    contactList.push(decryptedContact);
                }
                else if (contact.contactType === ContactType.CLOUD && contact.contactId) {
                    cloudContactsMap[contact.contactId] = contact;
                }
            }));
            if (Object.keys(cloudContactsMap).length) {
                const contactIdList = Object.keys(cloudContactsMap);
                const totalContacts = contactIdList.length;
                const MAX_CONTACTS_PER_QUERY = 50;
                for (let i = 0; i < totalContacts; i += MAX_CONTACTS_PER_QUERY) {
                    try {
                        const contactIdListChunk = contactIdList.slice(i, i + MAX_CONTACTS_PER_QUERY);
                        const query = contactIdListChunk.map((item) => `${SCIM_ID_FILTER} "${item}"`).join(OR);
                        const result = await scimQuery(query);
                        const slicedCloudContactsMap = Object.fromEntries(Object.entries(cloudContactsMap).slice(i, i + MAX_CONTACTS_PER_QUERY));
                        const resolvedContacts = this.resolveCloudContacts(slicedCloudContactsMap, result.body);
                        if (resolvedContacts) {
                            resolvedContacts.forEach((item) => contactList.push(item));
                        }
                    }
                    catch (error) {
                        log.warn(`Error processing contact chunk ${i}-${i + MAX_CONTACTS_PER_QUERY}`, loggerContext);
                    }
                }
            }
            await Promise.all(groups.map(async (group, idx) => {
                groups[idx].displayName = await this.webex.internal.encryption.decryptText(group.encryptionKeyUrl, group.displayName);
            }));
            this.groups = groups;
            this.contacts = contactList;
            const contactResponse = {
                statusCode: Number(response[STATUS_CODE]),
                data: {
                    contacts: contactList,
                    groups,
                },
                message: SUCCESS_MESSAGE,
            };
            log.log('Successfully fetched contacts and groups', loggerContext);
            return contactResponse;
        }
        catch (err) {
            log.error(`Error fetching contacts: ${JSON.stringify(err)}`, loggerContext);
            const errorStatus = serviceErrorCodeHandler(err, loggerContext);
            await uploadLogs();
            return errorStatus;
        }
    }
    async createNewEncryptionKeyUrl() {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.CREATE_NEW_ENCRYPTION_KEY_URL,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        let unboundedKeyUri = '';
        log.info('Requesting kms for a new KRO and key', loggerContext);
        const unboundedKeys = await this.webex.internal.encryption.kms.createUnboundKeys({ count: 1 });
        unboundedKeyUri = unboundedKeys[0].uri;
        this.webex.internal.encryption.kms.createResource({ keyUris: [unboundedKeyUri] });
        return unboundedKeyUri;
    }
    async fetchEncryptionKeyUrl() {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.FETCH_ENCRYPTION_KEY_URL,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        if (this.encryptionKeyUrl) {
            return this.encryptionKeyUrl;
        }
        if (this.groups === undefined) {
            await this.getContacts();
        }
        if (this.groups && this.groups.length) {
            return this.groups[0].encryptionKeyUrl;
        }
        this.encryptionKeyUrl = await this.createNewEncryptionKeyUrl();
        log.log(`Creating a default group: ${DEFAULT_GROUP_NAME}`, {
            file: CONTACTS_CLIENT,
            method: this.fetchEncryptionKeyUrl.name,
        });
        const response = await this.createContactGroup(DEFAULT_GROUP_NAME, this.encryptionKeyUrl);
        if (response.data.group?.groupId) {
            this.defaultGroupId = response.data.group?.groupId;
            log.log(`Successfully created default group with ID: ${this.defaultGroupId}`, {
                file: CONTACTS_CLIENT,
                method: this.fetchEncryptionKeyUrl.name,
            });
        }
        return this.encryptionKeyUrl;
    }
    async fetchDefaultGroup() {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.FETCH_DEFAULT_GROUP,
        };
        log.info(METHOD_START_MESSAGE, loggerContext);
        if (this.defaultGroupId) {
            log.log(`Using existing default group with ID: ${this.defaultGroupId}`, {
                file: CONTACTS_CLIENT,
                method: this.fetchDefaultGroup.name,
            });
            return this.defaultGroupId;
        }
        if (this.groups && this.groups.length) {
            for (let i = 0; i < this.groups.length; i += 1) {
                if (this.groups[i].displayName === DEFAULT_GROUP_NAME) {
                    this.defaultGroupId = this.groups[i].groupId;
                    log.log(`Found default group with ID: ${this.defaultGroupId}`, {
                        file: CONTACTS_CLIENT,
                        method: this.fetchDefaultGroup.name,
                    });
                    return this.defaultGroupId;
                }
            }
        }
        log.log('No default group found.', {
            file: CONTACTS_CLIENT,
            method: this.fetchDefaultGroup.name,
        });
        const response = await this.createContactGroup(DEFAULT_GROUP_NAME);
        const { group } = response.data;
        if (group) {
            const groupId = group.groupId;
            log.log(`Successfully created new default group with ID: ${groupId}`, {
                file: CONTACTS_CLIENT,
                method: this.fetchDefaultGroup.name,
            });
            return groupId;
        }
        return '';
    }
    async createContactGroup(displayName, encryptionKeyUrl, groupType) {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.CREATE_CONTACT_GROUP,
        };
        log.info(`${METHOD_START_MESSAGE} with displayName: ${displayName}`, loggerContext);
        const encryptionKeyUrlFinal = encryptionKeyUrl || (await this.fetchEncryptionKeyUrl());
        if (this.groups === undefined) {
            await this.getContacts();
        }
        if (this.groups && this.groups.length) {
            const isExistingGroup = this.groups.find((group) => {
                return group.displayName === displayName;
            });
            if (isExistingGroup) {
                log.warn(`Group name ${displayName} already exists.`, loggerContext);
                return {
                    statusCode: 400,
                    data: { error: 'Group displayName already exists' },
                    message: FAILURE_MESSAGE,
                };
            }
        }
        const encryptedDisplayName = await this.webex.internal.encryption.encryptText(encryptionKeyUrlFinal, displayName);
        const groupInfo = {
            schemas: CONTACTS_SCHEMA,
            displayName: encryptedDisplayName,
            groupType: groupType || GroupType.NORMAL,
            encryptionKeyUrl: encryptionKeyUrlFinal,
        };
        try {
            const response = await this.webex.request({
                uri: `${this.contactsServiceUrl}/${ENCRYPT_FILTER}/${USERS}/${GROUP_FILTER}`,
                method: HTTP_METHODS.POST,
                body: groupInfo,
            });
            log.log(`Response code: ${response.statusCode}`, loggerContext);
            log.log(`Response trackingId: ${response?.headers?.trackingid}`, loggerContext);
            const group = response.body;
            group.displayName = displayName;
            const contactResponse = {
                statusCode: Number(response[STATUS_CODE]),
                data: {
                    group,
                },
                message: SUCCESS_MESSAGE,
            };
            this.groups?.push(group);
            log.log(`Contact group ${displayName} successfully created`, loggerContext);
            return contactResponse;
        }
        catch (err) {
            log.error(`Unable to create contact group: ${JSON.stringify(err)}`, loggerContext);
            const errorStatus = serviceErrorCodeHandler(err, loggerContext);
            await uploadLogs();
            return errorStatus;
        }
    }
    async deleteContactGroup(groupId) {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.DELETE_CONTACT_GROUP,
        };
        log.info(`${METHOD_START_MESSAGE} with groupId: ${groupId}`, loggerContext);
        try {
            log.info(`Deleting contact group: ${groupId}`, loggerContext);
            const response = await this.webex.request({
                uri: `${this.contactsServiceUrl}/${ENCRYPT_FILTER}/${USERS}/${GROUP_FILTER}/${groupId}`,
                method: HTTP_METHODS.DELETE,
            });
            log.log(`Response trackingId: ${response?.headers?.trackingid}`, loggerContext);
            const contactResponse = {
                statusCode: Number(response[STATUS_CODE]),
                data: {},
                message: SUCCESS_MESSAGE,
            };
            const groupToDelete = this.groups?.findIndex((group) => group.groupId === groupId);
            if (groupToDelete !== undefined && groupToDelete !== -1) {
                this.groups?.splice(groupToDelete, 1);
            }
            if (!this.groups?.length) {
                this.defaultGroupId = '';
            }
            log.log(`Contact group ${groupId} successfully deleted`, loggerContext);
            return contactResponse;
        }
        catch (err) {
            log.error(`Unable to delete contact group ${groupId}: ${JSON.stringify(err)}`, loggerContext);
            const errorStatus = serviceErrorCodeHandler(err, loggerContext);
            await uploadLogs();
            return errorStatus;
        }
    }
    async createContact(contactInfo) {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.CREATE_CONTACT,
        };
        log.info(`${METHOD_START_MESSAGE} with contactType: ${contactInfo.contactType}`, loggerContext);
        try {
            const contact = { ...contactInfo };
            if (!contact.encryptionKeyUrl) {
                contact.encryptionKeyUrl = await this.fetchEncryptionKeyUrl();
            }
            if (!contact.groups || contact.groups.length === 0) {
                const defaultGroupId = await this.fetchDefaultGroup();
                contact.groups = [defaultGroupId];
            }
            contact.schemas = CONTACTS_SCHEMA;
            let requestBody = {};
            switch (contact.contactType) {
                case ContactType.CUSTOM: {
                    const encryptedContact = await this.encryptContact(contact);
                    requestBody = encryptedContact;
                    break;
                }
                case ContactType.CLOUD: {
                    if (!contact.contactId) {
                        return {
                            statusCode: 400,
                            data: {
                                error: 'contactId is required for contactType:CLOUD.',
                            },
                            message: FAILURE_MESSAGE,
                        };
                    }
                    const encryptedContact = await this.encryptContact(contact);
                    requestBody = encryptedContact;
                    break;
                }
                default: {
                    return {
                        statusCode: 400,
                        data: {
                            error: 'Unknown contactType received.',
                        },
                        message: FAILURE_MESSAGE,
                    };
                }
            }
            const response = await this.webex.request({
                uri: `${this.contactsServiceUrl}/${ENCRYPT_FILTER}/${USERS}/${CONTACT_FILTER}`,
                method: HTTP_METHODS.POST,
                body: requestBody,
            });
            log.log(`Response code: ${response.statusCode}`, loggerContext);
            log.log(`Response trackingId: ${response?.headers?.trackingid}`, loggerContext);
            const newContact = response.body;
            contact.contactId = newContact.contactId;
            const contactResponse = {
                statusCode: Number(response[STATUS_CODE]),
                data: {
                    contact,
                },
                message: SUCCESS_MESSAGE,
            };
            if (contact.contactType === ContactType.CLOUD && newContact.contactId) {
                const query = `${SCIM_ID_FILTER} "${newContact.contactId}"`;
                const res = await scimQuery(query);
                const resolvedContact = this.resolveCloudContacts(Object.fromEntries([[newContact.contactId, newContact]]), res.body);
                if (resolvedContact) {
                    this.contacts?.push(resolvedContact[0]);
                }
            }
            else {
                this.contacts?.push(contact);
            }
            log.log(`Contact successfully created`, loggerContext);
            return contactResponse;
        }
        catch (err) {
            log.error(`Failed to create contact: ${JSON.stringify(err)}`, loggerContext);
            const errorStatus = serviceErrorCodeHandler(err, loggerContext);
            await uploadLogs();
            return errorStatus;
        }
    }
    async deleteContact(contactId) {
        const loggerContext = {
            file: CONTACTS_CLIENT,
            method: METHODS.DELETE_CONTACT,
        };
        log.info(`${METHOD_START_MESSAGE} with contactId: ${contactId}`, loggerContext);
        try {
            log.info(`Deleting contact : ${contactId}`, loggerContext);
            const response = await this.webex.request({
                uri: `${this.contactsServiceUrl}/${ENCRYPT_FILTER}/${USERS}/${CONTACT_FILTER}/${contactId}`,
                method: HTTP_METHODS.DELETE,
            });
            const contactResponse = {
                statusCode: Number(response[STATUS_CODE]),
                data: {},
                message: SUCCESS_MESSAGE,
            };
            const contactToDelete = this.contacts?.findIndex((contact) => contact.contactId === contactId);
            if (contactToDelete !== undefined && contactToDelete !== -1) {
                this.contacts?.splice(contactToDelete, 1);
            }
            log.log(`Contact ${contactId} successfully deleted`, loggerContext);
            return contactResponse;
        }
        catch (err) {
            log.error(`Unable to delete contact ${contactId}: ${JSON.stringify(err)}`, loggerContext);
            const errorStatus = serviceErrorCodeHandler(err, loggerContext);
            await uploadLogs();
            return errorStatus;
        }
    }
    getSDKConnector() {
        return this.sdkConnector;
    }
}
export const createContactsClient = (webex, logger) => new ContactsClient(webex, logger);
