"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BusinessLogicMap = void 0;
const path_parser_1 = require("path-parser");
// eslint-disable-next-line import/default
const ramda_1 = __importDefault(require("ramda"));
const __1 = require("..");
const envs_1 = require("../envs");
const errors_1 = require("../errors");
const messaging_1 = require("../messaging");
const message_signing_1 = require("../messaging/message-signing");
const utils_1 = require("../utils");
const decorators_1 = require("./decorators");
const incomingEvents_1 = require("./incomingEvents");
const parseApiGatewayEvent_1 = require("./parseApiGatewayEvent");
const createPathTester = (httpPath) => ramda_1.default.memoizeWith(ramda_1.default.identity, (incomingHttpPath) => path_parser_1.Path.createPath(httpPath).partialTest(incomingHttpPath, {}));
const removePathTrailingSlash = (path) => (path.length > 1 ? path.replace(/\/$/, '') : path);
const removeQueryFromPath = (path) => path.split('?')[0];
const getCleanPathWithoutQuery = (path) => removePathTrailingSlash(removeQueryFromPath(path));
class BusinessLogicMap {
    constructor() {
        this.httpMap = [];
        this.sqsQueueMap = [];
        this.graphQlMap = [];
        this.eventHandlerMap = {};
        this.cronJobHandlerMap = {};
        this.handleRabbitMqEvent = async (busEvent, context, options) => {
            const messages = Object.entries(busEvent.rmqMessagesByQueue || {})
                .map(([queue, messages]) => {
                const queueName = queue.split('::')[0];
                return messages.map((m) => (Object.assign(Object.assign({}, m), { queueName })));
            })
                .flat();
            const handleMessages = messages.map(async (m) => {
                const messageParsed = (0, message_signing_1.parseMessageFromBase64String)(m.data);
                const redeliveredCount = (0, message_signing_1.getRedeliveredCount)(m);
                try {
                    await this.handleRabbitMqMessage(m, context, options);
                }
                catch (error) {
                    __1.logger.error(`Error while handling event: ${error}`, { payload: { error } });
                    __1.logger.error(`Will put message it in dlq`);
                    const bus = await (0, messaging_1.getBus)();
                    await bus.publish(messageParsed, {
                        queueName: (0, message_signing_1.getDeadLetterQueueName)(m.queueName),
                        redeliveredCount,
                    });
                }
            });
            await Promise.all(handleMessages);
            return {
                statusCode: 200,
                body: JSON.stringify({ processed: messages.length }),
            };
        };
        this.handleRabbitMqMessage = async (message, context, options) => {
            var _a, _b, _c;
            const topic = message.basicProperties.type;
            if (!(topic in this.eventHandlerMap))
                throw new Error('Topic handler not found');
            const dfnsMessage = (0, message_signing_1.parseMessageFromBase64String)(message.data);
            const redeliveredCount = (0, message_signing_1.getRedeliveredCount)(message);
            const publisherServiceName = message.basicProperties.appId;
            __1.logger.info(`Received message: ${dfnsMessage === null || dfnsMessage === void 0 ? void 0 : dfnsMessage.topic}`, { payload: { message, dfnsMessage } });
            if (!((_c = (_b = (_a = message === null || message === void 0 ? void 0 : message.basicProperties) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.Signature) === null || _c === void 0 ? void 0 : _c.bytes)) {
                throw Error('Message is not signed');
            }
            const signature = message.basicProperties.headers.Signature.bytes;
            const isSignatureValid = await (0, message_signing_1.isValidSignature)(Buffer.from(signature).toString(), JSON.stringify(dfnsMessage), publisherServiceName);
            if (!isSignatureValid) {
                throw new Error(`Cannot verify signature from publisher service ${publisherServiceName}`);
            }
            let handler = this.eventHandlerMap[topic];
            if (!handler) {
                throw Error(`No handlers found for event topic ${topic}`);
            }
            handler = (0, decorators_1.decorateRabbitMqEvent)(handler, options);
            return handler(dfnsMessage, {
                context,
                queueName: message.queueName,
                redeliveredCount: redeliveredCount,
                basicProperties: message.basicProperties,
            });
        };
        this.handleCronJobEvent = async (cronJobEvent, context) => {
            const handler = this.cronJobHandlerMap[cronJobEvent.eventTopic];
            if (!handler) {
                throw Error(`Cron event ${cronJobEvent.eventTopic} is not supported, no handlers defined`);
            }
            return handler(cronJobEvent, context);
        };
        this.handleApiGatewayEvent = this.handleApiGatewayEvent.bind(this);
        this.handleRabbitMqEvent = this.handleRabbitMqEvent.bind(this);
        this.handleSQSEvent = this.handleSQSEvent.bind(this);
        this.handleCronJobEvent = this.handleCronJobEvent.bind(this);
    }
    registerOperation(businessLogic, mappings) {
        const { http, graphQl } = mappings;
        if ((0, utils_1.isObject)(http)) {
            const { method, path } = http;
            this.httpMap.push({
                httpMethod: method,
                httpPath: path,
                pathTester: createPathTester(path),
                businessLogic: businessLogic,
            });
            // order from longest to shortest path
            this.httpMap.sort((a, b) => {
                // this handles the query params case
                const withoutQueryA = getCleanPathWithoutQuery(a.httpPath);
                const withoutQueryB = getCleanPathWithoutQuery(b.httpPath);
                return withoutQueryB.length - withoutQueryA.length;
            });
        }
        else if ((0, utils_1.isObject)(graphQl)) {
            const { businessDomain, operationName } = graphQl;
            this.graphQlMap.push([businessDomain, operationName, businessLogic]);
        }
        else {
            throw new Error('Not implement, unable to parse');
        }
    }
    registerSqsEventHandler(queueArn, handler, options = {}) {
        this.sqsQueueMap.push(Object.assign({ queueArn, handler }, options));
    }
    registerEventHandler(topic, eventHandler, options) {
        const maxRetries = (options === null || options === void 0 ? void 0 : options.maxRetries) || messaging_1.DEFAULT_MAX_MESSAGE_RETRIES;
        this.eventHandlerMap[topic] = (0, decorators_1.withMessageMaxRetry)(eventHandler, maxRetries);
    }
    registerCronJobHandler(topic, cronJobEventHandler) {
        this.cronJobHandlerMap[topic] = cronJobEventHandler;
    }
    async handleSQSEvent(sqsEvent, context) {
        const records = sqsEvent.Records;
        if (!records.length) {
            throw errors_1.TechnicalError.emptySqsMessages();
        }
        const sqsMapping = this.resolveSQSEventHandler(records[0].eventSourceARN);
        const batchItemFailures = [];
        const messageExecutions = records.map(async (record) => {
            const messageRaw = record.body;
            try {
                // It is assumed that every message passed in SQS are JSON strings
                const messageParsed = JSON.parse(messageRaw);
                await sqsMapping.handler(messageParsed, { lambdaContext: context, sqsEvent, record });
            }
            catch (err) {
                batchItemFailures.push({ itemIdentifier: record.messageId });
                __1.logger.error((err === null || err === void 0 ? void 0 : err.message) || 'Error', { payload: { message: messageRaw } });
            }
        });
        switch (sqsMapping.batchExecution) {
            case 'sequential':
                for (const messageExecution of messageExecutions) {
                    await messageExecution;
                }
                break;
            case 'parallel':
            default:
                await Promise.all(messageExecutions);
                break;
        }
        return { batchItemFailures };
    }
    async handleApiGatewayEvent(event, context) {
        const { body, authDetails } = await (0, parseApiGatewayEvent_1.parseApiGatewayEvent)(event);
        if (!authDetails.authBlock && (0, utils_1.isOrgEmployeeIdentity)(authDetails.identity)) {
            envs_1.EnvVars.setAuthenticatedUserId(authDetails.identity.employeeId);
        }
        const { query, businessLogic } = this.resolveHttpEvent(event);
        return businessLogic({
            body,
            query,
            authDetails,
            lambdaContext: { event, context },
        });
    }
    resolveHttpEvent(event) {
        const incomingHttpPath = (0, incomingEvents_1.getApiGatewayEventHttpPath)(event);
        const incomingHttpMethod = (0, incomingEvents_1.getApiGatewayEventHttpMethod)(event);
        // exact match only happens if no parameter in url
        const exactMatch = this.httpMap.find(({ httpMethod, httpPath }) => {
            const isExactMatch = getCleanPathWithoutQuery(incomingHttpPath) === getCleanPathWithoutQuery(httpPath);
            return httpMethod.toUpperCase() === incomingHttpMethod.toUpperCase() && isExactMatch;
        });
        const shallowMatch = this.httpMap.find(({ httpMethod, pathTester }) => httpMethod === incomingHttpMethod && (0, utils_1.isObject)(pathTester(incomingHttpPath)));
        const match = exactMatch || shallowMatch;
        if (!match) {
            throw errors_1.ServiceError.httpOperationNotFound(incomingHttpMethod, incomingHttpPath);
        }
        const queryParams = (0, parseApiGatewayEvent_1.extractQueryParamsFromApiGatewayEvent)(event);
        const pathParams = (0, parseApiGatewayEvent_1.extractPathParamsFromApiGatewayEvent)(event);
        const query = Object.assign(Object.assign(Object.assign({}, queryParams), pathParams), match.pathTester(incomingHttpPath));
        return {
            businessLogic: match.businessLogic,
            query,
        };
    }
    resolveSQSEventHandler(messageQueueArn) {
        const mapping = this.sqsQueueMap.find(({ queueArn }) => queueArn === messageQueueArn) ||
            this.sqsQueueMap.find(({ queueArn }) => queueArn instanceof RegExp && queueArn.test(messageQueueArn));
        if (!mapping) {
            throw errors_1.ServiceError.sqsHandlerNotFound(messageQueueArn);
        }
        return mapping;
    }
}
exports.BusinessLogicMap = BusinessLogicMap;
//# sourceMappingURL=BusinessLogicMap.js.map