"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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FetchService = exports.RETRIABLE_ERRORS = void 0;
const uniproxy_1 = require("@ubio/uniproxy");
const form_data_1 = __importDefault(require("form-data"));
const inversify_1 = require("inversify");
const node_fetch_1 = __importDefault(require("node-fetch"));
const querystring_1 = __importDefault(require("querystring"));
const uuid_1 = require("uuid");
const logger_js_1 = require("../../logger.js");
const util = __importStar(require("../util/index.js"));
const blob_js_1 = require("./blob.js");
const browser_js_1 = require("./browser.js");
const proxy_js_1 = require("./proxy.js");
exports.RETRIABLE_ERRORS = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'ERR_STREAM_DESTROYED'];
let FetchService = class FetchService {
    constructor(blobs, logger, browser, proxy) {
        this.blobs = blobs;
        this.logger = logger;
        this.browser = browser;
        this.proxy = proxy;
    }
    async send(_spec) {
        const spec = await this.prepareRequestSpec(_spec);
        return await this.sendWithRetries(spec);
    }
    async prepareRequestSpec(spec) {
        const mode = spec.mode || 'Fetch';
        const requestBodyFormat = spec.requestBodyFormat || 'none';
        const responseBodyFormat = spec.responseBodyFormat || 'none';
        const retries = typeof spec.retries === 'number' ? spec.retries : 3;
        const timeout = typeof spec.timeout === 'number' ? spec.timeout : 120000;
        const method = String(spec.method || 'GET').toUpperCase();
        const url = util.formatUrl(spec);
        const { body, headers: bodyHeaders } = await this.prepareRequestBody(spec);
        const headers = Object.assign({}, bodyHeaders, this.prepareRequestHeaders(spec));
        return {
            mode,
            requestBodyFormat,
            responseBodyFormat,
            retries,
            timeout,
            method,
            url,
            headers,
            body,
        };
    }
    prepareRequestHeaders(spec) {
        const { headers = {} } = spec;
        const result = {};
        for (const key of Object.keys(headers)) {
            result[key.toLowerCase()] = headers[key];
        }
        return result;
    }
    async prepareRequestBody(spec) {
        const { body = {} } = spec;
        switch (spec.requestBodyFormat) {
            case 'json':
                return {
                    body: JSON.stringify(body),
                    headers: {
                        'content-type': 'application/json; charset=utf-8',
                    },
                };
            case 'urlencoded':
                return {
                    body: querystring_1.default.stringify(body),
                    headers: {
                        'content-type': 'application/x-www-form-urlencoded',
                    },
                };
            case 'text':
                return {
                    body: typeof body === 'object' ? JSON.stringify(body) : String(body),
                    headers: {
                        'content-type': 'text/plain',
                    },
                };
            case 'multipart':
                return await this.prepareMultipartRequestBody(spec);
            default:
                return {
                    body: '',
                    headers: {},
                };
        }
    }
    async prepareMultipartRequestBody(spec) {
        let body = spec.body || {};
        if (typeof body !== 'object') {
            body = {};
        }
        util.checkType(body, 'object', 'body');
        const formData = new form_data_1.default();
        for (const key of Object.keys(body)) {
            const value = body[key];
            if (util.getType(value) === 'object') {
                const { blobId, filename } = value;
                util.checkType(blobId, 'string', `body.${key}.blobId`);
                util.checkType(filename, 'string', `body.${key}.filename`);
                const buffer = await this.blobs.readBlob(blobId);
                formData.append(key, buffer, { filename });
            }
            else {
                formData.append(key, String(value));
            }
        }
        const headers = formData.getHeaders();
        const buffer = await util.streamToBuffer(formData);
        return {
            headers,
            body: buffer.toString('binary'),
        };
    }
    async sendWithRetries(spec) {
        let attempts = spec.retries;
        let lastError;
        while (attempts > 0) {
            attempts -= 1;
            try {
                return await this.sendSingleRequest(spec);
            }
            catch (error) {
                lastError = error;
                const canRetry = exports.RETRIABLE_ERRORS.includes(error.code);
                if (!canRetry) {
                    break;
                }
                this.logger.warn('Fetch failed, retrying', { error });
            }
        }
        throw lastError;
    }
    async sendSingleRequest(spec) {
        switch (spec.mode) {
            case 'Fetch':
                return await this.sendFetchRequestWithTimeout(spec);
            case 'Node':
            case 'Node+Proxy':
            default:
                return await this.sendNodeRequest(spec, spec.mode === 'Node+Proxy');
        }
    }
    async sendFetchRequestWithTimeout(spec) {
        return await Promise.race([
            this.sendFetchRequest(spec),
            new Promise((_, reject) => {
                setTimeout(() => {
                    const err = util.createError({
                        code: 'FetchTimeout',
                        message: 'Fetch request timed out',
                        retry: false,
                    });
                    reject(err);
                }, spec.timeout);
            }),
        ]);
    }
    async sendFetchRequest(spec) {
        const page = this.browser.page;
        const response = (await page.evaluateJson(async (request) => {
            const { method, url, headers, body } = request;
            const payload = ['GET', 'HEAD'].includes(method) ? undefined : String(body);
            const res = await window.fetch(url, {
                method,
                headers,
                body: payload,
                mode: 'cors',
            });
            const arrayBuffer = await res.arrayBuffer();
            const bytes = new Uint8Array(arrayBuffer);
            let binary = '';
            for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            const bodyBase64 = btoa(binary);
            const result = {
                url: res.url,
                status: res.status,
                statusText: res.statusText,
                headers: {},
                body: bodyBase64,
            };
            for (const [k, v] of res.headers) {
                result.headers[k] = v;
            }
            return result;
        }, spec));
        const bodyBuffer = Buffer.from(response.body, 'base64');
        response.body = await this.parseResponseBody(spec, bodyBuffer);
        return response;
    }
    async sendNodeRequest(spec, useProxy) {
        const { method, url, headers, body } = spec;
        const parsedUrl = new URL(url);
        const payload = ['GET', 'HEAD'].includes(method) ? undefined : String(body);
        const fetchOptions = {
            method,
            headers,
            body: payload,
            timeout: spec.timeout,
        };
        let agent = null;
        if (useProxy) {
            const proxyPort = this.proxy.getProxyPort();
            const ca = this.proxy.getCACertificates();
            agent =
                parsedUrl.protocol === 'https:'
                    ? new uniproxy_1.HttpsProxyAgent({ host: `127.0.0.1:${proxyPort}` }, { ca })
                    : new uniproxy_1.HttpProxyAgent({ host: `127.0.0.1:${proxyPort}` });
            fetchOptions.agent = agent;
        }
        try {
            const res = await (0, node_fetch_1.default)(url, fetchOptions);
            const buffer = await res.buffer();
            const response = {
                url: res.url,
                status: res.status,
                statusText: res.statusText,
                headers: {},
                body: await this.parseResponseBody(spec, buffer),
            };
            for (const [k, v] of res.headers) {
                response.headers[k] = v;
            }
            return response;
        }
        finally {
            if (agent) {
                agent.destroy();
            }
        }
    }
    async parseResponseBody(spec, buffer) {
        if (spec.responseBodyFormat === 'blob') {
            return await this.blobs.createBlob((0, uuid_1.v4)(), buffer);
        }
        return util.parseBodyData(buffer, spec.responseBodyFormat);
    }
};
FetchService = __decorate([
    (0, inversify_1.injectable)(),
    __param(0, (0, inversify_1.inject)(blob_js_1.BlobService)),
    __param(1, (0, inversify_1.inject)(logger_js_1.Logger)),
    __param(2, (0, inversify_1.inject)(browser_js_1.BrowserService)),
    __param(3, (0, inversify_1.inject)(proxy_js_1.ProxyService)),
    __metadata("design:paramtypes", [blob_js_1.BlobService,
        logger_js_1.Logger,
        browser_js_1.BrowserService,
        proxy_js_1.ProxyService])
], FetchService);
exports.FetchService = FetchService;
//# sourceMappingURL=fetch.js.map