"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeTunnelClient = void 0;
const logger_1 = require("@applitools/logger");
const req_1 = require("@applitools/req");
//@ts-ignore
const execution_grid_tunnel_1 = require("@applitools/execution-grid-tunnel");
const abort_controller_1 = require("abort-controller");
const utils = __importStar(require("@applitools/utils"));
const RETRY_BACKOFF = [
    ...Array(5).fill(2000),
    ...Array(4).fill(5000),
    10000, // all next tries with delay 10s
];
function makeTunnelClient({ settings, logger: defaultLogger, } = {}) {
    var _a, _b, _c;
    const logger = (0, logger_1.makeLogger)({ logger: defaultLogger, format: { label: 'tunnel-client' } });
    const tunnelServerUrl = (_c = (_b = (_a = settings === null || settings === void 0 ? void 0 : settings.tunnelServerUrl) !== null && _a !== void 0 ? _a : utils.general.getEnvValue('EXECUTION_CLOUD_URL')) !== null && _b !== void 0 ? _b : utils.general.getEnvValue('EG_SERVER_URL')) !== null && _c !== void 0 ? _c : 'https://exec-wus.applitools.com';
    const getTunnelService = utils.general.cachify(async (tunnelServerUrl) => {
        if (settings === null || settings === void 0 ? void 0 : settings.serviceUrl) {
            return { url: settings.serviceUrl, close: () => Promise.resolve() };
        }
        const { port, cleanupFunction } = await (0, execution_grid_tunnel_1.startEgTunnelService)({ egTunnelManagerUrl: tunnelServerUrl, logger });
        return {
            url: `http://localhost:${port}`,
            async close() {
                await cleanupFunction();
                getTunnelService.clearCache();
            },
        };
    });
    const tunnels = new Map();
    const queues = new Map();
    return { list, create, destroy, replace, close };
    async function list() {
        var _a, _b;
        const service = await getTunnelService(tunnelServerUrl);
        const response = await (0, req_1.req)('/tunnels', {
            method: 'GET',
            baseUrl: service.url,
        });
        const body = await response.json().catch(() => null);
        if (response.status === 200)
            return body.map((tunnelId) => { var _a; return (_a = tunnels.get(tunnelId)) !== null && _a !== void 0 ? _a : { tunnelId }; });
        logger.error(`Failed to create tunnel with status ${response.status} and code ${(_a = body === null || body === void 0 ? void 0 : body.message) !== null && _a !== void 0 ? _a : 'UNKNOWN_ERROR'}`);
        throw new Error(`Failed to create tunnel with code ${(_b = body === null || body === void 0 ? void 0 : body.message) !== null && _b !== void 0 ? _b : 'UNKNOWN_ERROR'}`);
    }
    async function create(credentials) {
        var _a;
        const service = await getTunnelService((_a = credentials.tunnelServerUrl) !== null && _a !== void 0 ? _a : tunnelServerUrl);
        const queueKey = `${credentials.apiKey}$${credentials.eyesServerUrl}`;
        let queue = queues.get(queueKey);
        if (!queue) {
            queue = utils.queues.makeCorkableQueue({ makeAbortController: () => new abort_controller_1.AbortController() });
            queues.set(queueKey, queue);
        }
        return queue.run(task);
        async function task(signal, attempt = 1) {
            var _a, _b;
            if (signal.aborted)
                return queue.pause;
            const response = await (0, req_1.req)('/tunnels', {
                method: 'POST',
                baseUrl: service.url,
                headers: {
                    'x-eyes-api-key': credentials.apiKey,
                    'x-eyes-server-url': credentials.eyesServerUrl,
                },
                // TODO uncomment when we can throw different abort reasons for task cancelation and timeout abortion
                // signal,
            });
            const body = await response.json().catch(() => null);
            if (['CONCURRENCY_LIMIT_REACHED', 'NO_AVAILABLE_TUNNEL_PROXY'].includes(body === null || body === void 0 ? void 0 : body.message)) {
                queue.cork();
                // after query is corked the task might be aborted
                if (signal.aborted)
                    return queue.pause;
                await utils.general.sleep(RETRY_BACKOFF[Math.min(attempt, RETRY_BACKOFF.length - 1)]);
                return task(signal, attempt + 1);
            }
            else {
                queue.uncork();
                if (response.status === 201) {
                    const tunnel = { tunnelId: body, credentials };
                    tunnels.set(tunnel.tunnelId, tunnel);
                    return tunnel;
                }
                logger.error(`Failed to create tunnel with status ${response.status} and code ${(_a = body === null || body === void 0 ? void 0 : body.message) !== null && _a !== void 0 ? _a : 'UNKNOWN_ERROR'}`);
                throw new Error(`Failed to create tunnel with code ${(_b = body === null || body === void 0 ? void 0 : body.message) !== null && _b !== void 0 ? _b : 'UNKNOWN_ERROR'}`);
            }
        }
    }
    async function replace(tunnelId) {
        const tunnel = tunnels.get(tunnelId);
        if (!tunnel) {
            logger.error(`Failed to replace tunnel using unknown tunnel id "${tunnelId}"`);
            throw new Error(`Failed to replace tunnel using unknown tunnel id "${tunnelId}"`);
        }
        await destroy(tunnel.tunnelId);
        return create(tunnel.credentials);
    }
    async function destroy(tunnelId, options) {
        var _a, _b, _c;
        const tunnel = tunnels.get(tunnelId);
        if (!tunnel) {
            logger.error(`Failed to delete tunnel using unknown tunnel id "${tunnelId}"`);
            throw new Error(`Failed to delete tunnel using unknown tunnel id "${tunnelId}"`);
        }
        const service = await getTunnelService((_a = tunnel.credentials.tunnelServerUrl) !== null && _a !== void 0 ? _a : tunnelServerUrl);
        const response = await (0, req_1.req)(`/tunnels/${tunnel.tunnelId}`, {
            method: 'DELETE',
            query: { reason: options === null || options === void 0 ? void 0 : options.reason },
            baseUrl: service.url,
            headers: {
                'x-eyes-api-key': tunnel.credentials.apiKey,
                'x-eyes-server-url': tunnel.credentials.eyesServerUrl,
            },
        });
        const body = await response.json().catch(() => null);
        if (response.status === 200) {
            tunnels.delete(tunnel.tunnelId);
            return;
        }
        logger.error(`Failed to delete tunnel with status ${response.status} and code ${(_b = body === null || body === void 0 ? void 0 : body.message) !== null && _b !== void 0 ? _b : 'UNKNOWN_ERROR'}`);
        throw new Error(`Failed to delete tunnel with code ${(_c = body === null || body === void 0 ? void 0 : body.message) !== null && _c !== void 0 ? _c : 'UNKNOWN_ERROR'}`);
    }
    async function close() {
        try {
            const tunnels = await list();
            logger.log(tunnels);
            await Promise.all(tunnels.map(tunnel => destroy(tunnel.tunnelId, { reason: 'client-closed' })));
            const service = await getTunnelService(tunnelServerUrl);
            await service.close();
        }
        catch (error) {
            logger.error(`Failed to close tunnel client due to an error`, error);
        }
    }
}
exports.makeTunnelClient = makeTunnelClient;
