"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendData = exports.getOrCreateProducer = exports.validate = exports.getTopics = exports.serializeKafkaConfig = exports.producersByConfig = void 0;
exports.isValidHostPort = isValidHostPort;
const kafkajs_1 = require("kafkajs");
const actions_core_1 = require("@segment/actions-core");
const types_1 = require("./types");
const constants_1 = require("./constants");
exports.producersByConfig = {};
const serializeKafkaConfig = (settings) => {
    const config = {
        clientId: settings.clientId,
        brokers: settings.brokers
            .trim()
            .split(',')
            .map((b) => b.trim())
            .sort(),
        mechanism: settings.mechanism,
        username: settings.username,
        password: settings.password,
        accessKeyId: settings.accessKeyId,
        secretAccessKey: settings.secretAccessKey,
        authorizationIdentity: settings.authorizationIdentity,
        ssl_ca: settings.ssl_ca,
        ssl_cert: settings.ssl_cert,
        ssl_key: settings.ssl_key,
        ssl_reject_unauthorized_ca: settings.ssl_reject_unauthorized_ca,
        ssl_enabled: settings.ssl_enabled
    };
    return JSON.stringify(config);
};
exports.serializeKafkaConfig = serializeKafkaConfig;
const getTopics = async (settings) => {
    const kafka = getKafka(settings);
    const admin = kafka.admin();
    await admin.connect();
    const topics = await admin.listTopics();
    await admin.disconnect();
    return { choices: topics.map((topic) => ({ label: topic, value: topic })) };
};
exports.getTopics = getTopics;
const getKafka = (settings) => {
    const kafkaConfig = {
        clientId: settings.clientId,
        brokers: settings.brokers
            .trim()
            .split(',')
            .map((broker) => broker.trim()),
        requestTimeout: constants_1.PRODUCER_REQUEST_TIMEOUT_MS,
        sasl: (() => {
            switch (settings.mechanism) {
                case 'plain':
                    return {
                        username: settings?.username,
                        password: settings?.password,
                        mechanism: settings.mechanism
                    };
                case 'scram-sha-256':
                case 'scram-sha-512':
                    return {
                        username: settings.username,
                        password: settings.password,
                        mechanism: settings.mechanism
                    };
                case 'aws':
                    return {
                        accessKeyId: settings.accessKeyId,
                        secretAccessKey: settings.secretAccessKey,
                        authorizationIdentity: settings.authorizationIdentity,
                        mechanism: settings.mechanism
                    };
                default:
                    return undefined;
            }
        })(),
        ssl: (() => {
            if (settings?.ssl_ca) {
                const ssl = {
                    ca: [`-----BEGIN CERTIFICATE-----\n${settings.ssl_ca.trim()}\n-----END CERTIFICATE-----`],
                    rejectUnauthorized: settings.ssl_reject_unauthorized_ca
                };
                if (settings.mechanism === 'client-cert-auth') {
                    ;
                    (ssl.key = `-----BEGIN PRIVATE KEY-----\n${settings?.ssl_key?.trim()}\n-----END PRIVATE KEY-----`),
                        (ssl.cert = `-----BEGIN CERTIFICATE-----\n${settings?.ssl_cert?.trim()}\n-----END CERTIFICATE-----`);
                }
                return ssl;
            }
            else if (settings.ssl_enabled) {
                return settings.ssl_enabled;
            }
            return undefined;
        })(),
        retry: {
            retries: 0
        }
    };
    try {
        return new kafkajs_1.Kafka(kafkaConfig);
    }
    catch (error) {
        throw new actions_core_1.IntegrationError(`Kafka Connection Error: ${error.message}`, 'KAFKA_CONNECTION_ERROR', 400);
    }
};
const validate = (settings) => {
    if (['plain', 'scram-sha-256', 'scram-sha-512'].includes(settings.mechanism) &&
        (!settings.username || !settings.password)) {
        throw new actions_core_1.IntegrationError('Username and Password are required for PLAIN and SCRAM authentication mechanisms', 'SASL_PARAMS_MISSING', 400);
    }
    if (['aws'].includes(settings.mechanism) && (!settings.accessKeyId || !settings.secretAccessKey)) {
        throw new actions_core_1.IntegrationError('AWS Access Key ID and AWS Secret Key are required for AWS authentication mechanism', 'SASL_AWS_PARAMS_MISSING', 400);
    }
    if (['client-cert-auth'].includes(settings.mechanism) && (!settings.ssl_key || !settings.ssl_cert)) {
        throw new actions_core_1.IntegrationError('SSL Client Key and SSL Client Certificate are required for Client Certificate authentication mechanism', 'SSL_CLIENT_CERT_AUTH_PARAMS_MISSING', 400);
    }
    const isValidBroker = settings.brokers
        .split(',')
        .map((item) => item.trim())
        .filter((item) => item.length > 0)
        .every(isValidHostPort);
    if (!isValidBroker) {
        throw new actions_core_1.IntegrationError('Brokers must be in the format host:port', 'BROKER_FORMAT_INVALID', 400);
    }
};
exports.validate = validate;
const getProducer = (settings) => {
    return getKafka(settings).producer({
        createPartitioner: kafkajs_1.Partitioners.DefaultPartitioner
    });
};
const getOrCreateProducer = async (settings, statsContext) => {
    const key = (0, exports.serializeKafkaConfig)(settings);
    const now = Date.now();
    const cached = exports.producersByConfig[key];
    if (cached) {
        const isExpired = now - cached.lastUsed > constants_1.PRODUCER_TTL_MS;
        if (!isExpired) {
            cached.lastUsed = now;
            statsContext?.statsClient?.incr('kafka_connection_reused', 1, statsContext?.tags);
            await cached.producer.connect();
            return cached.producer;
        }
        if (cached.isConnected) {
            try {
                statsContext?.statsClient?.incr('kafka_connection_closed', 1, statsContext?.tags);
                await cached.producer.disconnect();
            }
            catch {
                statsContext?.statsClient?.incr('kafka_disconnect_error', 1, statsContext?.tags);
            }
        }
        delete exports.producersByConfig[key];
    }
    const kafka = getKafka(settings);
    const producer = kafka.producer({ createPartitioner: kafkajs_1.Partitioners.DefaultPartitioner });
    await producer.connect();
    statsContext?.statsClient?.incr('kafka_connection_opened', 1, statsContext?.tags);
    exports.producersByConfig[key] = {
        producer,
        isConnected: true,
        lastUsed: now
    };
    return producer;
};
exports.getOrCreateProducer = getOrCreateProducer;
function getKafkaError(error) {
    const errorCause = error?.cause;
    if (errorCause) {
        return errorCause;
    }
    return error;
}
function isValidHostPort(brokerStr) {
    if (typeof brokerStr !== 'string')
        return false;
    const [host, port] = brokerStr.split(':');
    if (!host || !port)
        return false;
    const portNum = Number(port);
    return typeof host === 'string' && host.length > 0 && Number.isInteger(portNum) && portNum >= 0 && portNum <= 65535;
}
const sendData = async (settings, payload, features, statsContext, logger) => {
    (0, exports.validate)(settings);
    const groupedPayloads = {};
    const set = new Set();
    payload.forEach((p) => {
        const { topic, partition, default_partition } = p;
        if (!groupedPayloads[topic]) {
            groupedPayloads[topic] = [];
        }
        groupedPayloads[topic].push(p);
        set.add(`${topic}-${partition}-${default_partition}`);
    });
    const statsClient = statsContext?.statsClient;
    const tags = statsContext?.tags;
    statsClient?.histogram('kafka.configurable_batch_keys.unique_keys', set.size, tags);
    const topicMessages = Object.keys(groupedPayloads).map((topic) => ({
        topic,
        messages: groupedPayloads[topic].map((payload) => ({
            value: JSON.stringify(payload.payload),
            key: payload.key,
            headers: payload?.headers ?? undefined,
            partition: payload?.partition ?? payload?.default_partition ?? undefined,
            partitionerType: types_1.DEFAULT_PARTITIONER
        }))
    }));
    let producer;
    try {
        if (features && features[constants_1.FLAGON_NAME]) {
            producer = await (0, exports.getOrCreateProducer)(settings, statsContext);
        }
        else {
            producer = getProducer(settings);
            await producer.connect();
        }
    }
    catch (error) {
        if (error.name !== 'IntegrationError') {
            const kafkaError = getKafkaError(error);
            logger?.crit(`Kafka Connection Error - ${kafkaError.name} | ${JSON.stringify(kafkaError)}`);
            throw new actions_core_1.IntegrationError(`Kafka Connection Error - ${kafkaError.name}: ${kafkaError.message}`, kafkaError.name, 500);
        }
        else {
            logger?.crit(`Kafka Connection Error - ${error.name}: ${error}`);
            throw error;
        }
    }
    for (const data of topicMessages) {
        try {
            const batch_bytes = Buffer.byteLength(JSON.stringify(data.messages), 'utf8');
            statsClient?.histogram('kafka.batch_bytes', batch_bytes, tags);
            await producer.send(data);
        }
        catch (error) {
            const kafkaError = getKafkaError(error);
            logger?.crit(`Kafka Send Error - ${kafkaError.name} | ${JSON.stringify(kafkaError)}`);
            throw new actions_core_1.IntegrationError(`Kafka Producer Error - ${kafkaError.name}: ${kafkaError.message}`, kafkaError.name, 500);
        }
        finally {
            if (features && features[constants_1.FLAGON_NAME]) {
                const key = (0, exports.serializeKafkaConfig)(settings);
                if (exports.producersByConfig[key]) {
                    exports.producersByConfig[key].lastUsed = Date.now();
                }
            }
            else {
                await producer?.disconnect();
            }
        }
    }
};
exports.sendData = sendData;
//# sourceMappingURL=utils.js.map