"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sendMessage = exports.stopServer = exports.startServer = exports.newHttpError = void 0;
const http_1 = require("http");
const node_fetch_1 = require("node-fetch");
const Logger_1 = require("./Logger");
const fs_extra_1 = require("fs-extra");
const promises_1 = require("fs/promises");
const crypto_1 = require("./crypto");
const tcpPortUsed = require('tcp-port-used');
const maxPorts = 10;
const findAvailablePort = async (startPort) => {
    for (let i = 0; i < 100; i++) {
        const port = startPort + i;
        const inUse = await tcpPortUsed.check(port, 'localhost');
        if (!inUse)
            return port;
    }
    throw new Error(`All potential ports are in use or not available. Starting from port: ${startPort}`);
};
const findListenerPorts = async (startPort) => {
    const output = [];
    for (let i = 0; i < maxPorts; i++) {
        const port = startPort + i;
        const inUse = await tcpPortUsed.check(port, 'localhost');
        if (inUse)
            output.push(port);
    }
    return output;
};
const parseJson = (req) => {
    return new Promise((resolve, reject) => {
        let body = '';
        req.on('data', chunk => {
            body += chunk;
        });
        req.on('end', () => {
            try {
                resolve(JSON.parse(body));
            }
            catch (error) {
                reject(error);
            }
        });
    });
};
const newHttpError = (httpCode, message = '') => {
    const error = new Error(message);
    error.httpCode = httpCode;
    return error;
};
exports.newHttpError = newHttpError;
const getSecretKey = async (filePath) => {
    try {
        const keyLength = 64;
        const writeKeyToFile = async () => {
            const key = (0, crypto_1.getSecureRandomString)(keyLength);
            await (0, promises_1.writeFile)(filePath, key, 'utf-8');
            return key;
        };
        if (!(await (0, fs_extra_1.pathExists)(filePath))) {
            return await writeKeyToFile();
        }
        const key = await (0, promises_1.readFile)(filePath, 'utf-8');
        if (key.length !== keyLength)
            return await writeKeyToFile();
        return key;
    }
    catch (error) {
        const e = error;
        e.message = `Could not get secret key from file: ${filePath}`;
        throw e;
    }
};
// `secretKeyFilePath` must be the same for all the instances that can communicate with each others
const startServer = async (startPort, secretKeyFilePath, messageHandler, options = null) => {
    const logger = options && options.logger ? options.logger : new Logger_1.default();
    let port;
    try {
        port = await findAvailablePort(startPort);
    }
    catch (error) {
        logger.error(`Could not find available - using default: ${startPort}`, error);
        port = startPort;
    }
    const secretKey = await getSecretKey(secretKeyFilePath);
    return new Promise((resolve, reject) => {
        let promiseFulfilled = false;
        try {
            const server = (0, http_1.createServer)(async (req, res) => {
                let message = null;
                try {
                    message = await parseJson(req);
                    if (message.secretKey !== secretKey)
                        throw (0, exports.newHttpError)(401, 'Invalid secret key');
                    if (!message.action)
                        throw (0, exports.newHttpError)(400, 'Missing "action" property in message');
                    const response = await messageHandler(message);
                    res.writeHead(200, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify(response));
                }
                catch (error) {
                    const httpError = error;
                    const httpCode = httpError.httpCode || 500;
                    logger.error('Could not response to request:', message, 'Error', httpCode, httpError.message);
                    res.writeHead(httpCode, { 'Content-Type': 'text/plain' });
                    res.end(`Error ${httpCode}: ${httpError.message}`);
                }
            });
            server.on('error', error => {
                logger.error('Server error:', error);
                if (!promiseFulfilled) {
                    promiseFulfilled = true;
                    reject(error);
                }
            });
            server.listen(port, 'localhost', () => {
                if (!promiseFulfilled) {
                    promiseFulfilled = true;
                    resolve({
                        httpServer: server,
                        port,
                        secretKey,
                    });
                }
            });
        }
        catch (error) {
            if (!promiseFulfilled) {
                promiseFulfilled = true;
                reject(error);
            }
            else {
                logger.error('Server initialization error:', error);
            }
        }
    });
};
exports.startServer = startServer;
const stopServer = async (server) => {
    if (!server)
        return;
    return new Promise((resolve, reject) => {
        server.httpServer.close((error) => {
            if (error) {
                reject(error);
            }
            else {
                resolve();
            }
        });
    });
};
exports.stopServer = stopServer;
const sendMessage = async (startPort, message, options = null) => {
    const logger = options && options.logger ? options.logger : new Logger_1.default();
    const output = [];
    let ports = [];
    try {
        ports = await findListenerPorts(startPort);
    }
    catch (error) {
        logger.error(`Could not find listener ports - using default only: ${startPort}`, error);
        ports.push(startPort);
    }
    const sendToSpecificPortOnly = !!options && !!options.sendToSpecificPortOnly;
    for (const port of ports) {
        if (sendToSpecificPortOnly && port !== startPort)
            continue;
        if (message.sourcePort === port)
            continue;
        try {
            const response = await (0, node_fetch_1.default)(`http://localhost:${port}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(message),
            });
            if (!response.ok) {
                // It means the server doesn't support this particular message - so just skip it
                if (response.status === 404)
                    continue;
                const text = await response.text();
                throw new Error(`Request failed: on port ${port}: ${text}`);
            }
            output.push({
                port,
                response: await response.json(),
            });
        }
        catch (error) {
            logger.error(`Could not send message on port ${port}:`, error);
        }
    }
    return output;
};
exports.sendMessage = sendMessage;
//# sourceMappingURL=ipc.js.map