"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const url = require("url");
const os_1 = require("os");
const helpers = require("./helpers");
const zlib = require("zlib");
const util = require("util");
const constants_1 = require("./constants");
const request = require("request");
class HttpClient {
    constructor($logger, $processService, $proxyService, $staticConfig) {
        this.$logger = $logger;
        this.$processService = $processService;
        this.$proxyService = $proxyService;
        this.$staticConfig = $staticConfig;
        this.cleanupData = [];
        this.$processService.attachToProcessExitSignals(this, () => {
            this.cleanupData.forEach(d => {
                this.cleanupAfterRequest(d);
            });
        });
    }
    httpRequest(options, proxySettings) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const result = yield this.httpRequestCore(options, proxySettings);
                return result;
            }
            catch (err) {
                if (err.message === HttpClient.STUCK_REQUEST_ERROR_MESSAGE || err.message === HttpClient.STUCK_RESPONSE_ERROR_MESSAGE) {
                    this.$logger.warn("%s Retrying request to %s...", err.message, options.url || options);
                    const retryResult = yield this.httpRequestCore(options, proxySettings);
                    return retryResult;
                }
                throw err;
            }
        });
    }
    httpRequestCore(options, proxySettings) {
        return __awaiter(this, void 0, void 0, function* () {
            if (_.isString(options)) {
                options = {
                    url: options,
                    method: "GET"
                };
            }
            const clonedOptions = _.cloneDeep(options);
            if (clonedOptions.url) {
                const urlParts = url.parse(clonedOptions.url);
                if (urlParts.protocol) {
                    clonedOptions.proto = urlParts.protocol.slice(0, -1);
                }
                clonedOptions.host = urlParts.hostname;
                clonedOptions.port = urlParts.port;
                clonedOptions.path = urlParts.path;
            }
            const requestProto = clonedOptions.proto || "http";
            const body = clonedOptions.body;
            delete clonedOptions.body;
            let pipeTo = options.pipeTo;
            delete clonedOptions.pipeTo;
            const cliProxySettings = yield this.$proxyService.getCache();
            clonedOptions.headers = clonedOptions.headers || {};
            const headers = clonedOptions.headers;
            yield this.useProxySettings(proxySettings, cliProxySettings, clonedOptions, headers, requestProto);
            if (!headers.Accept || headers.Accept.indexOf("application/json") < 0) {
                if (headers.Accept) {
                    headers.Accept += ", ";
                }
                else {
                    headers.Accept = "";
                }
                headers.Accept += "application/json; charset=UTF-8, */*;q=0.8";
            }
            if (!headers["User-Agent"]) {
                if (!this.defaultUserAgent) {
                    this.defaultUserAgent = `${this.$staticConfig.USER_AGENT_NAME}/${this.$staticConfig.version} (Node.js ${process.versions.node}; ${process.platform}; ${process.arch})`;
                    this.$logger.debug("User-Agent: %s", this.defaultUserAgent);
                }
                headers["User-Agent"] = this.defaultUserAgent;
            }
            if (!headers["Accept-Encoding"]) {
                headers["Accept-Encoding"] = "gzip,deflate";
            }
            const result = new Promise((resolve, reject) => {
                let timerId;
                const cleanupRequestData = Object.create({ timers: [] });
                this.cleanupData.push(cleanupRequestData);
                const promiseActions = {
                    resolve,
                    reject,
                    isResolved: () => false
                };
                clonedOptions.url = clonedOptions.url || `${clonedOptions.proto}://${clonedOptions.host}${clonedOptions.path}`;
                if (clonedOptions.timeout) {
                    timerId = setTimeout(() => {
                        this.setResponseResult(promiseActions, cleanupRequestData, { err: new Error(`Request to ${clonedOptions.url} timed out.`) });
                    }, clonedOptions.timeout);
                    cleanupRequestData.timers.push(timerId);
                    delete clonedOptions.timeout;
                }
                clonedOptions.encoding = null;
                clonedOptions.followAllRedirects = true;
                this.$logger.trace("httpRequest: %s", util.inspect(clonedOptions));
                const requestObj = request(clonedOptions);
                cleanupRequestData.req = requestObj;
                requestObj
                    .on("error", (err) => {
                    this.$logger.trace("An error occurred while sending the request:", err);
                    const errorMessageMatch = err.message.match(HttpClient.STATUS_CODE_REGEX);
                    const errorMessageStatusCode = errorMessageMatch && errorMessageMatch[1] && +errorMessageMatch[1];
                    const errorMessage = this.getErrorMessage(errorMessageStatusCode, null);
                    err.proxyAuthenticationRequired = errorMessageStatusCode === constants_1.HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED;
                    err.message = errorMessage || err.message;
                    this.setResponseResult(promiseActions, cleanupRequestData, { err });
                })
                    .on("socket", (s) => {
                    let stuckRequestTimerId;
                    stuckRequestTimerId = setTimeout(() => {
                        this.setResponseResult(promiseActions, cleanupRequestData, { err: new Error(HttpClient.STUCK_REQUEST_ERROR_MESSAGE) });
                    }, clonedOptions.timeout || HttpClient.STUCK_REQUEST_TIMEOUT);
                    cleanupRequestData.timers.push(stuckRequestTimerId);
                    s.once("secureConnect", () => {
                        clearTimeout(stuckRequestTimerId);
                        stuckRequestTimerId = null;
                    });
                })
                    .on("response", (response) => {
                    cleanupRequestData.res = response;
                    let lastChunkTimestamp = Date.now();
                    cleanupRequestData.stuckResponseIntervalId = setInterval(() => {
                        if (Date.now() - lastChunkTimestamp > HttpClient.STUCK_RESPONSE_CHECK_INTERVAL) {
                            this.setResponseResult(promiseActions, cleanupRequestData, { err: new Error(HttpClient.STUCK_RESPONSE_ERROR_MESSAGE) });
                        }
                    }, HttpClient.STUCK_RESPONSE_CHECK_INTERVAL);
                    const successful = helpers.isRequestSuccessful(response);
                    if (!successful) {
                        pipeTo = undefined;
                    }
                    let responseStream = response;
                    responseStream.on("data", (chunk) => {
                        lastChunkTimestamp = Date.now();
                    });
                    switch (response.headers["content-encoding"]) {
                        case "gzip":
                            responseStream = responseStream.pipe(zlib.createGunzip());
                            break;
                        case "deflate":
                            responseStream = responseStream.pipe(zlib.createInflate());
                            break;
                    }
                    if (pipeTo) {
                        pipeTo.on("finish", () => {
                            this.$logger.trace("httpRequest: Piping done. code = %d", response.statusCode.toString());
                            this.setResponseResult(promiseActions, cleanupRequestData, { response });
                        });
                        responseStream.pipe(pipeTo);
                    }
                    else {
                        const data = [];
                        responseStream.on("data", (chunk) => {
                            data.push(chunk);
                        });
                        responseStream.on("end", () => {
                            this.$logger.trace("httpRequest: Done. code = %d", response.statusCode.toString());
                            const responseBody = data.join("");
                            if (successful) {
                                this.setResponseResult(promiseActions, cleanupRequestData, { body: responseBody, response });
                            }
                            else {
                                const errorMessage = this.getErrorMessage(response.statusCode, responseBody);
                                const err = new Error(errorMessage);
                                err.response = response;
                                err.body = responseBody;
                                this.setResponseResult(promiseActions, cleanupRequestData, { err });
                            }
                        });
                    }
                });
                this.$logger.trace("httpRequest: Sending:\n%s", this.$logger.prepare(body));
                if (!body || !body.pipe) {
                    requestObj.end(body);
                }
                else {
                    body.pipe(requestObj);
                }
            });
            const response = yield result;
            if (helpers.isResponseRedirect(response.response)) {
                const unmodifiedOptions = _.cloneDeep(options);
                if (response.response.statusCode === constants_1.HttpStatusCodes.SEE_OTHER) {
                    unmodifiedOptions.method = "GET";
                }
                this.$logger.trace("Begin redirected to %s", response.headers.location);
                unmodifiedOptions.url = response.headers.location;
                return yield this.httpRequest(unmodifiedOptions);
            }
            return response;
        });
    }
    setResponseResult(result, cleanupRequestData, resultData) {
        this.cleanupAfterRequest(cleanupRequestData);
        if (!result.isResolved()) {
            result.isResolved = () => true;
            if (resultData.err || !resultData.response.complete) {
                return result.reject(resultData.err || new Error("Request canceled"));
            }
            const finalResult = resultData;
            finalResult.headers = resultData.response.headers;
            result.resolve(finalResult);
        }
    }
    getErrorMessage(statusCode, body) {
        if (statusCode === constants_1.HttpStatusCodes.PROXY_AUTHENTICATION_REQUIRED) {
            const clientNameLowerCase = this.$staticConfig.CLIENT_NAME.toLowerCase();
            this.$logger.error(`You can run ${os_1.EOL}\t${clientNameLowerCase} proxy set <url> <username> <password>.${os_1.EOL}In order to supply ${clientNameLowerCase} with the credentials needed.`);
            return "Your proxy requires authentication.";
        }
        else if (statusCode === constants_1.HttpStatusCodes.PAYMENT_REQUIRED) {
            return util.format("Your subscription has expired.");
        }
        else {
            this.$logger.trace("Request was unsuccessful. Server returned: ", body);
            try {
                const err = JSON.parse(body);
                if (_.isString(err)) {
                    return err;
                }
                if (err && err.ExceptionMessage) {
                    return err.ExceptionMessage;
                }
                if (err && err.Message) {
                    return err.Message;
                }
            }
            catch (parsingFailed) {
                this.$logger.trace("Failed to get error from http request: ", parsingFailed);
                return `The server returned unexpected response: ${body}`;
            }
            return body;
        }
    }
    useProxySettings(proxySettings, cliProxySettings, options, headers, requestProto) {
        return __awaiter(this, void 0, void 0, function* () {
            if (proxySettings || cliProxySettings) {
                const proto = (proxySettings && proxySettings.protocol) || cliProxySettings.protocol || "http:";
                const host = (proxySettings && proxySettings.hostname) || cliProxySettings.hostname;
                const port = (proxySettings && proxySettings.port) || cliProxySettings.port;
                let credentialsPart = "";
                if (cliProxySettings.username && cliProxySettings.password) {
                    credentialsPart = `${cliProxySettings.username}:${cliProxySettings.password}@`;
                }
                options.proxy = `${proto}//${credentialsPart}${host}:${port}`;
                options.rejectUnauthorized = proxySettings ? proxySettings.rejectUnauthorized : cliProxySettings.rejectUnauthorized;
                this.$logger.trace("Using proxy: %s", options.proxy);
            }
        });
    }
    cleanupAfterRequest(data) {
        data.timers.forEach(t => {
            if (t) {
                clearTimeout(t);
                t = null;
            }
        });
        if (data.stuckResponseIntervalId) {
            clearInterval(data.stuckResponseIntervalId);
            data.stuckResponseIntervalId = null;
        }
        if (data.req) {
            data.req.abort();
        }
        if (data.res) {
            data.res.destroy();
        }
    }
}
HttpClient.STATUS_CODE_REGEX = /statuscode=(\d+)/i;
HttpClient.STUCK_REQUEST_ERROR_MESSAGE = "The request can't receive any response.";
HttpClient.STUCK_RESPONSE_ERROR_MESSAGE = "Can't receive all parts of the response.";
HttpClient.STUCK_REQUEST_TIMEOUT = 60000;
HttpClient.STUCK_RESPONSE_CHECK_INTERVAL = 10000;
exports.HttpClient = HttpClient;
$injector.register("httpClient", HttpClient);
