"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const actions_core_1 = require("@segment/actions-core");
const versioning_info_1 = require("../versioning-info");
const constants_1 = require("../constants");
const action = {
    title: 'Upsert Profile',
    description: 'Create or update Memora profiles. If a profile already exists, its traits are merged (new keys added, existing keys overwritten). Supports batching up to 1000 profiles.',
    defaultSubscription: 'type = "identify"',
    fields: {
        enable_batching: {
            label: 'Enable Batching',
            description: 'Enable batching of requests to Memora. Batches can contain up to 1000 profiles.',
            type: 'boolean',
            default: true,
            unsafe_hidden: true
        },
        batch_size: {
            label: 'Batch Size',
            description: 'Maximum number of profiles to include in each batch. Actual batch sizes may be lower.',
            type: 'number',
            default: 1000,
            unsafe_hidden: true
        },
        memora_store: {
            label: 'Memora Store',
            description: 'The Memora Store ID to use for this profile. This should be a valid Memora Store associated with your Twilio account.',
            type: 'string',
            required: true,
            dynamic: true,
            disabledInputMethods: ['literal', 'variable', 'function', 'enrichment', 'freeform']
        },
        profile_identifiers: {
            label: 'Profile Identifiers',
            description: 'Profile identifiers from all trait groups. At runtime, each event must contain at least one identifier with a non-null value, and at least two total non-null fields across identifiers and traits combined. Events with sparse data (e.g., only one identifier present) will be rejected. These fields are dynamically loaded from the selected Memora Store. When manually entering keys, use the format "TraitGroupName.$.traitName" (e.g., "Contact.$.email", "Contact.$.phone").',
            type: 'object',
            required: true,
            additionalProperties: true,
            dynamic: true,
            defaultObjectUI: 'keyvalue'
        },
        profile_traits: {
            label: 'Profile Traits',
            description: 'Traits for the profile from all trait groups. While this field is optional in the mapping configuration, at runtime each event must have at least two total non-null fields across identifiers and traits combined. If you map only one identifier, you must also map at least one trait that will have a value for your events. These fields are dynamically loaded from the selected Memora Store. When manually entering keys, use the format "TraitGroupName.$.traitName" (e.g., "Contact.$.firstName", "PurchaseHistory.$.lastPurchaseDate").',
            type: 'object',
            required: false,
            additionalProperties: true,
            dynamic: true,
            defaultObjectUI: 'keyvalue'
        }
    },
    dynamicFields: {
        memora_store: async (request, { settings }) => {
            return fetchMemoraStores(request, settings);
        },
        profile_identifiers: {
            __keys__: async (request, { settings, payload }) => {
                if (!payload.memora_store) {
                    return { choices: [], error: { message: 'Please select a Memora Store first', code: 'STORE_REQUIRED' } };
                }
                const result = await fetchTraitGroupFields(request, settings, payload.memora_store);
                return result.identifiers;
            }
        },
        profile_traits: {
            __keys__: async (request, { settings, payload }) => {
                if (!payload.memora_store) {
                    return { choices: [], error: { message: 'Please select a Memora Store first', code: 'STORE_REQUIRED' } };
                }
                const result = await fetchTraitGroupFields(request, settings, payload.memora_store);
                return result.traits;
            }
        }
    },
    perform: async (request, { payload, settings, logger, statsContext, personasContext }) => {
        const { rawResponse, multiStatus } = await upsertProfiles(request, [payload], settings, logger, statsContext, personasContext);
        if (multiStatus.isErrorResponseAtIndex(0)) {
            const response = multiStatus.getResponseAtIndex(0).value();
            const error = response;
            throw new actions_core_1.PayloadValidationError(error.errormessage || 'Invalid profile');
        }
        if (!rawResponse) {
            throw new actions_core_1.IntegrationError('No response returned from bulk upsert', 'MISSING_RESPONSE', 500);
        }
        return rawResponse;
    },
    performBatch: async (request, { payload: payloads, settings, logger, statsContext, personasContext }) => {
        const { multiStatus } = await upsertProfiles(request, payloads, settings, logger, statsContext, personasContext);
        return multiStatus;
    }
};
async function upsertProfiles(request, payloads, settings, logger, statsContext, personasContext) {
    if (!payloads || payloads.length === 0) {
        throw new actions_core_1.IntegrationError('No profiles provided', 'EMPTY_BATCH', 400);
    }
    const storeId = payloads[0].memora_store;
    const validProfiles = [];
    const validIndices = [];
    const invalidIndices = [];
    const validationErrors = new Map();
    payloads.forEach((payload, index) => {
        const identifiers = payload.profile_identifiers || {};
        const identifierCount = Object.values(identifiers).filter((v) => v !== undefined && v !== null).length;
        const hasIdentifier = identifierCount > 0;
        const traits = (payload.profile_traits && typeof payload.profile_traits === 'object' ? payload.profile_traits : {});
        const traitCount = Object.values(traits).filter((v) => v !== undefined && v !== null).length;
        const totalFields = identifierCount + traitCount;
        if (!hasIdentifier) {
            invalidIndices.push(index);
            validationErrors.set(index, 'Profile must contain at least one identifier');
            return;
        }
        if (totalFields < 2) {
            invalidIndices.push(index);
            validationErrors.set(index, 'Profile must contain at least two total fields (identifiers + traits). It could be two identifiers, or one identifier and one trait.');
            return;
        }
        try {
            const traitGroups = buildTraitGroups(payload);
            validProfiles.push({ traits: traitGroups });
            validIndices.push(index);
        }
        catch (error) {
            invalidIndices.push(index);
            validationErrors.set(index, error instanceof Error ? error.message : String(error));
        }
    });
    const statsTags = buildStatsTags(settings, storeId, personasContext, statsContext?.tags);
    const tagStr = statsTags.join(', ');
    if (invalidIndices.length > 0) {
        logger?.warn?.(`Skipped ${invalidIndices.length} invalid profile(s). Processing ${validProfiles.length} valid profile(s). ${tagStr}`);
    }
    if (validProfiles.length === 0) {
        logger?.warn?.(`No valid profiles to import. All profiles failed validation. ${tagStr}`);
        statsContext?.statsClient?.incr('memora.upsert_profile.failure', invalidIndices.length, statsTags);
        const multiStatusResponse = new actions_core_1.MultiStatusResponse();
        invalidIndices.forEach((index) => {
            multiStatusResponse.setErrorResponseAtIndex(index, {
                status: 400,
                errormessage: validationErrors.get(index) || 'Invalid profile'
            });
        });
        return { rawResponse: undefined, multiStatus: multiStatusResponse };
    }
    try {
        const response = await request(`${constants_1.BASE_URL}/${versioning_info_1.API_VERSION}/Stores/${storeId}/Profiles/Bulk`, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
                'X-Pre-Auth-Context': settings.twilioAccount
            },
            username: settings.username,
            password: settings.password,
            json: {
                profiles: validProfiles
            }
        });
        const twilioRequestId = response.headers?.get?.('twilio-request-id');
        logger?.info?.(`Bulk upsert completed successfully for ${validProfiles.length} profile(s)${twilioRequestId ? `. twilio-request-id: ${twilioRequestId}` : ''}. ${tagStr}`);
        statsContext?.statsClient?.incr('memora.upsert_profile.success', validProfiles.length, statsTags);
        if (invalidIndices.length > 0) {
            statsContext?.statsClient?.incr('memora.upsert_profile.failure', invalidIndices.length, statsTags);
        }
        const multiStatusResponse = new actions_core_1.MultiStatusResponse();
        validIndices.forEach((index) => {
            multiStatusResponse.setSuccessResponseAtIndex(index, {
                status: response.status,
                sent: {},
                body: 'accepted'
            });
        });
        invalidIndices.forEach((index) => {
            multiStatusResponse.setErrorResponseAtIndex(index, {
                status: 400,
                errormessage: validationErrors.get(index) || 'Invalid profile'
            });
        });
        return { rawResponse: response, multiStatus: multiStatusResponse };
    }
    catch (error) {
        const twilioRequestId = error?.response?.headers?.get?.('twilio-request-id');
        logger?.error?.(`Error in bulk upsert: ${error instanceof Error ? error.message : String(error)}${twilioRequestId ? `. twilio-request-id: ${twilioRequestId}` : ''}. ${tagStr}`);
        statsContext?.statsClient?.incr('memora.upsert_profile.failure', payloads.length, statsTags);
        throw error;
    }
}
function buildStatsTags(settings, storeId, personasContext, existingTags) {
    const audienceKey = personasContext?.computation_key;
    const spaceId = personasContext?.['space_id'] != null ? String(personasContext['space_id']) : undefined;
    return [
        ...(existingTags ?? []),
        `twilioAccountId:${settings.twilioAccount}`,
        `memory_store_id:${storeId}`,
        ...(audienceKey ? [`audience_key:${audienceKey}`] : []),
        ...(spaceId ? [`space_id:${spaceId}`] : [])
    ];
}
function buildTraitGroups(payload) {
    const traitGroups = {};
    const invalidKeys = [];
    if (payload.profile_traits && typeof payload.profile_traits === 'object') {
        const traits = payload.profile_traits;
        Object.entries(traits).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                const match = key.match(/^([^.]+)\.\$\.(.+)$/);
                if (match) {
                    const traitGroupName = match[1];
                    const traitName = match[2];
                    if (!traitGroups[traitGroupName]) {
                        traitGroups[traitGroupName] = {};
                    }
                    traitGroups[traitGroupName][traitName] = value;
                }
                else {
                    invalidKeys.push(key);
                }
            }
        });
        if (invalidKeys.length > 0) {
            throw new actions_core_1.PayloadValidationError(`Invalid trait key format detected. The following keys do not match the expected format: ${invalidKeys.join(', ')}. ` +
                `Expected format: "TraitGroupName.$.traitName" (e.g., "Contact.$.firstName", "PurchaseHistory.$.lastPurchaseDate").`);
        }
    }
    if (payload.profile_identifiers && typeof payload.profile_identifiers === 'object') {
        const identifiers = payload.profile_identifiers;
        const invalidIdentifierKeys = [];
        Object.entries(identifiers).forEach(([key, value]) => {
            if (value !== undefined && value !== null) {
                const match = key.match(/^([^.]+)\.\$\.(.+)$/);
                if (match) {
                    const traitGroupName = match[1];
                    const traitName = match[2];
                    if (!traitGroups[traitGroupName]) {
                        traitGroups[traitGroupName] = {};
                    }
                    traitGroups[traitGroupName][traitName] = value;
                }
                else {
                    invalidIdentifierKeys.push(key);
                }
            }
        });
        if (invalidIdentifierKeys.length > 0) {
            throw new actions_core_1.PayloadValidationError(`Invalid identifier key format detected. The following keys do not match the expected format: ${invalidIdentifierKeys.join(', ')}. ` + `Expected format: "TraitGroupName.$.traitName" (e.g., "Contact.$.email", "Contact.$.phone").`);
        }
    }
    return traitGroups;
}
async function fetchTraitGroupFields(request, settings, storeId) {
    try {
        const traitGroupsResponse = await request(`${constants_1.BASE_URL}/${versioning_info_1.API_VERSION}/ControlPlane/Stores/${storeId}/TraitGroups?pageSize=100&includeTraits=true`, {
            method: 'GET',
            headers: {
                'X-Pre-Auth-Context': settings.twilioAccount
            },
            username: settings.username,
            password: settings.password,
            skipResponseCloning: true
        });
        const traitGroupObjects = traitGroupsResponse?.data?.traitGroups || [];
        const identifierChoices = [];
        const traitChoices = [];
        for (const traitGroup of traitGroupObjects) {
            const traitGroupName = traitGroup.displayName;
            const traits = traitGroup.traits || {};
            Object.entries(traits).forEach(([traitName, trait]) => {
                const value = `${traitGroupName}.$.${traitName}`;
                const label = `${traitGroupName}.${trait.displayName || traitName}`;
                if (trait.idTypePromotion && trait.dataType === 'STRING') {
                    const description = trait.description
                        ? trait.description
                        : `${traitGroupName} - ${trait.displayName} (${trait.idTypePromotion})`;
                    identifierChoices.push({ label, value, description });
                }
                else if (!trait.idTypePromotion && trait.dataType === 'STRING') {
                    const description = trait.description
                        ? trait.description
                        : `${traitGroupName} - ${trait.displayName} (${trait.dataType})`;
                    traitChoices.push({ label, value, description });
                }
            });
        }
        return {
            identifiers: { choices: identifierChoices },
            traits: { choices: traitChoices }
        };
    }
    catch (error) {
        const statusCode = error?.response?.status || 'unknown';
        const errorMsg = error?.response?.data?.message || (error instanceof Error ? error.message : String(error));
        const errorResult = (fieldType) => ({
            choices: [],
            error: {
                message: `Unable to fetch ${fieldType} (HTTP ${statusCode}: ${errorMsg}). You can still manually enter field names.`,
                code: 'FETCH_ERROR'
            }
        });
        return {
            identifiers: errorResult('identifiers'),
            traits: errorResult('traits')
        };
    }
}
async function fetchMemoraStores(request, settings) {
    try {
        const response = await request(`${constants_1.BASE_URL}/${versioning_info_1.API_VERSION}/ControlPlane/Stores?pageSize=100&orderBy=ASC`, {
            method: 'GET',
            headers: {
                'X-Pre-Auth-Context': settings.twilioAccount
            },
            username: settings.username,
            password: settings.password,
            skipResponseCloning: true
        });
        const stores = response?.data?.stores || [];
        const memoraStores = await Promise.all(stores.map((storeId) => {
            return request(`${constants_1.BASE_URL}/${versioning_info_1.API_VERSION}/ControlPlane/Stores/${storeId}`, {
                method: 'GET',
                headers: {
                    'X-Pre-Auth-Context': settings.twilioAccount
                },
                username: settings.username,
                password: settings.password,
                skipResponseCloning: true
            });
        }));
        const choices = memoraStores.map((store) => ({
            label: store.data?.displayName || store.data?.id,
            value: store.data?.id
        }));
        return {
            choices
        };
    }
    catch (error) {
        return {
            choices: [],
            error: {
                message: 'Unable to fetch memora stores. Please check your authentication credentials.',
                code: 'FETCH_ERROR'
            }
        };
    }
}
exports.default = action;
//# sourceMappingURL=index.js.map