"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());
    });
};
const url = require("url");
const helpers = require("./helpers");
const zlib = require("zlib");
const util = require("util");
const progress = require("progress-stream");
const filesize = require("filesize");
class HttpClient {
    constructor($logger, $staticConfig, $config) {
        this.$logger = $logger;
        this.$staticConfig = $staticConfig;
        this.$config = $config;
    }
    httpRequest(options, proxySettings) {
        return __awaiter(this, void 0, void 0, function* () {
            if (_.isString(options)) {
                options = {
                    url: options,
                    method: "GET"
                };
            }
            let unmodifiedOptions = _.clone(options);
            if (options.url) {
                let urlParts = url.parse(options.url);
                if (urlParts.protocol) {
                    options.proto = urlParts.protocol.slice(0, -1);
                }
                options.host = urlParts.hostname;
                options.port = urlParts.port;
                options.path = urlParts.path;
                delete options.url;
            }
            let requestProto = options.proto || "http";
            delete options.proto;
            let body = options.body;
            delete options.body;
            let pipeTo = options.pipeTo;
            delete options.pipeTo;
            let proto = this.$config.USE_PROXY ? "http" : requestProto;
            let http = require(proto);
            options.headers = options.headers || {};
            let headers = options.headers;
            if (proxySettings || this.$config.USE_PROXY) {
                options.path = requestProto + "://" + options.host + options.path;
                headers.Host = options.host;
                options.host = (proxySettings && proxySettings.hostname) || this.$config.PROXY_HOSTNAME;
                options.port = (proxySettings && proxySettings.port) || this.$config.PROXY_PORT;
                this.$logger.trace("Using proxy with host: %s, port: %d, path is: %s", options.host, options.port, options.path);
            }
            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 = util.format("%sCLI/%s (Node.js %s; %s; %s)", this.$staticConfig.CLIENT_NAME, this.$staticConfig.version, 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";
            }
            let result = new Promise((resolve, reject) => {
                let timerId;
                let promiseActions = {
                    resolve,
                    reject,
                    isResolved: () => false
                };
                if (options.timeout) {
                    timerId = setTimeout(() => {
                        this.setResponseResult(promiseActions, timerId, { err: new Error(`Request to ${unmodifiedOptions.url} timed out.`) });
                    }, options.timeout);
                    delete options.timeout;
                }
                this.$logger.trace("httpRequest: %s", util.inspect(options));
                let request = http.request(options, (response) => {
                    let data = [];
                    let isRedirect = helpers.isResponseRedirect(response);
                    let successful = helpers.isRequestSuccessful(response);
                    if (!successful) {
                        pipeTo = undefined;
                    }
                    let responseStream = response;
                    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, timerId, { response });
                        });
                        pipeTo = this.trackDownloadProgress(pipeTo);
                        responseStream.pipe(pipeTo);
                    }
                    else {
                        responseStream.on("data", (chunk) => {
                            data.push(chunk);
                        });
                        responseStream.on("end", () => {
                            this.$logger.trace("httpRequest: Done. code = %d", response.statusCode.toString());
                            let responseBody = data.join("");
                            if (successful || isRedirect) {
                                this.setResponseResult(promiseActions, timerId, { body: responseBody, response });
                            }
                            else {
                                let errorMessage = this.getErrorMessage(response, responseBody);
                                let err = new Error(errorMessage);
                                err.response = response;
                                err.body = responseBody;
                                this.setResponseResult(promiseActions, timerId, { err });
                            }
                        });
                    }
                });
                request.on("error", (err) => {
                    this.setResponseResult(promiseActions, timerId, { err });
                });
                this.$logger.trace("httpRequest: Sending:\n%s", this.$logger.prepare(body));
                if (!body || !body.pipe) {
                    request.end(body);
                }
                else {
                    body.pipe(request);
                }
            });
            let response = yield result;
            if (helpers.isResponseRedirect(response.response)) {
                if (response.response.statusCode === 303) {
                    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, timerId, resultData) {
        return __awaiter(this, void 0, void 0, function* () {
            if (timerId) {
                clearTimeout(timerId);
                timerId = null;
            }
            if (!result.isResolved()) {
                result.isResolved = () => true;
                if (resultData.err) {
                    return result.reject(resultData.err);
                }
                let finalResult = resultData;
                finalResult.headers = resultData.response.headers;
                result.resolve(finalResult);
            }
        });
    }
    trackDownloadProgress(pipeTo) {
        let lastMessageSize = 0, carriageReturn = "\x1B[0G", timeElapsed = 0;
        let progressStream = progress({ time: 1000 }, (progress) => {
            timeElapsed = progress.runtime;
            if (timeElapsed >= 1) {
                this.$logger.write("%s%s", carriageReturn, Array(lastMessageSize + 1).join(" "));
                let message = util.format("%sDownload progress ... %s | %s | %s/s", carriageReturn, Math.floor(progress.percentage) + "%", filesize(progress.transferred), filesize(progress.speed));
                this.$logger.write(message);
                lastMessageSize = message.length;
            }
        });
        progressStream.on("finish", () => {
            if (timeElapsed >= 1) {
                this.$logger.out("%s%s%s%s", carriageReturn, Array(lastMessageSize + 1).join(" "), carriageReturn, "Download completed.");
            }
        });
        progressStream.pipe(pipeTo);
        return progressStream;
    }
    getErrorMessage(response, body) {
        if (response.statusCode === 402) {
            let subscriptionUrl = util.format("%s://%s/appbuilder/account/subscription", this.$config.AB_SERVER_PROTO, this.$config.AB_SERVER);
            return util.format("Your subscription has expired. Go to %s to manage your subscription. Note: After you renew your subscription, " +
                "log out and log back in for the changes to take effect.", subscriptionUrl);
        }
        else {
            try {
                let err = JSON.parse(body);
                if (_.isString(err)) {
                    return err;
                }
                if (err.ExceptionMessage) {
                    return err.ExceptionMessage;
                }
                if (err.Message) {
                    return err.Message;
                }
            }
            catch (parsingFailed) {
                return `The server returned unexpected response: ${parsingFailed.toString()}`;
            }
            return body;
        }
    }
}
exports.HttpClient = HttpClient;
$injector.register("httpClient", HttpClient);
