"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = exports.setAccessToken = exports.setRefreshToken = void 0;
const url_1 = require("url");
const stream_1 = require("stream");
const proxy_agent_1 = require("proxy-agent");
const retry = require("retry");
const abort_controller_1 = require("abort-controller");
const node_fetch_1 = require("node-fetch");
const util_1 = require("util");
const auth = require("./auth");
const error_1 = require("./error");
const logger_1 = require("./logger");
const responseToError_1 = require("./responseToError");
const FormData = require("form-data");
const pkg = require("../package.json");
const CLI_VERSION = pkg.version;
const GOOG_QUOTA_USER = "x-goog-quota-user";
let accessToken = "";
let refreshToken = "";
function setRefreshToken(token = "") {
    refreshToken = token;
}
exports.setRefreshToken = setRefreshToken;
function setAccessToken(token = "") {
    accessToken = token;
}
exports.setAccessToken = setAccessToken;
function proxyURIFromEnv() {
    return (process.env.HTTPS_PROXY ||
        process.env.https_proxy ||
        process.env.HTTP_PROXY ||
        process.env.http_proxy ||
        undefined);
}
class Client {
    constructor(opts) {
        this.opts = opts;
        if (this.opts.auth === undefined) {
            this.opts.auth = true;
        }
        if (this.opts.urlPrefix.endsWith("/")) {
            this.opts.urlPrefix = this.opts.urlPrefix.substring(0, this.opts.urlPrefix.length - 1);
        }
    }
    get(path, options = {}) {
        const reqOptions = Object.assign(options, {
            method: "GET",
            path,
        });
        return this.request(reqOptions);
    }
    post(path, json, options = {}) {
        const reqOptions = Object.assign(options, {
            method: "POST",
            path,
            body: json,
        });
        return this.request(reqOptions);
    }
    patch(path, json, options = {}) {
        const reqOptions = Object.assign(options, {
            method: "PATCH",
            path,
            body: json,
        });
        return this.request(reqOptions);
    }
    put(path, json, options = {}) {
        const reqOptions = Object.assign(options, {
            method: "PUT",
            path,
            body: json,
        });
        return this.request(reqOptions);
    }
    delete(path, options = {}) {
        const reqOptions = Object.assign(options, {
            method: "DELETE",
            path,
        });
        return this.request(reqOptions);
    }
    async request(reqOptions) {
        if (!reqOptions.responseType) {
            reqOptions.responseType = "json";
        }
        if (reqOptions.responseType === "stream" && !reqOptions.resolveOnHTTPError) {
            throw new error_1.FirebaseError("apiv2 will not handle HTTP errors while streaming and you must set `resolveOnHTTPError` and check for res.status >= 400 on your own", { exit: 2 });
        }
        let internalReqOptions = Object.assign(reqOptions, {
            headers: new node_fetch_1.Headers(reqOptions.headers),
        });
        internalReqOptions = this.addRequestHeaders(internalReqOptions);
        if (this.opts.auth) {
            internalReqOptions = await this.addAuthHeader(internalReqOptions);
        }
        try {
            return await this.doRequest(internalReqOptions);
        }
        catch (thrown) {
            if (thrown instanceof error_1.FirebaseError) {
                throw thrown;
            }
            let err;
            if (thrown instanceof Error) {
                err = thrown;
            }
            else {
                err = new Error(thrown);
            }
            throw new error_1.FirebaseError(`Failed to make request: ${err.message}`, { original: err });
        }
    }
    addRequestHeaders(reqOptions) {
        if (!reqOptions.headers) {
            reqOptions.headers = new node_fetch_1.Headers();
        }
        reqOptions.headers.set("Connection", "keep-alive");
        if (!reqOptions.headers.has("User-Agent")) {
            reqOptions.headers.set("User-Agent", `FirebaseCLI/${CLI_VERSION}`);
        }
        reqOptions.headers.set("X-Client-Version", `FirebaseCLI/${CLI_VERSION}`);
        if (!reqOptions.headers.has("Content-Type")) {
            if (reqOptions.responseType === "json") {
                reqOptions.headers.set("Content-Type", "application/json");
            }
        }
        return reqOptions;
    }
    async addAuthHeader(reqOptions) {
        if (!reqOptions.headers) {
            reqOptions.headers = new node_fetch_1.Headers();
        }
        let token;
        if (isLocalInsecureRequest(this.opts.urlPrefix)) {
            token = "owner";
        }
        else {
            token = await this.getAccessToken();
        }
        reqOptions.headers.set("Authorization", `Bearer ${token}`);
        return reqOptions;
    }
    async getAccessToken() {
        if (accessToken) {
            return accessToken;
        }
        const data = (await auth.getAccessToken(refreshToken, []));
        return data.access_token;
    }
    requestURL(options) {
        const versionPath = this.opts.apiVersion ? `/${this.opts.apiVersion}` : "";
        return `${this.opts.urlPrefix}${versionPath}${options.path}`;
    }
    async doRequest(options) {
        var _a;
        if (!options.path.startsWith("/")) {
            options.path = "/" + options.path;
        }
        let fetchURL = this.requestURL(options);
        if (options.queryParams) {
            if (!(options.queryParams instanceof url_1.URLSearchParams)) {
                const sp = new url_1.URLSearchParams();
                for (const key of Object.keys(options.queryParams)) {
                    const value = options.queryParams[key];
                    sp.append(key, `${value}`);
                }
                options.queryParams = sp;
            }
            const queryString = options.queryParams.toString();
            if (queryString) {
                fetchURL += `?${queryString}`;
            }
        }
        const fetchOptions = {
            headers: options.headers,
            method: options.method,
            redirect: options.redirect,
            compress: options.compress,
        };
        if (proxyURIFromEnv()) {
            fetchOptions.agent = new proxy_agent_1.ProxyAgent();
        }
        if (options.signal) {
            fetchOptions.signal = options.signal;
        }
        let reqTimeout;
        if (options.timeout) {
            const controller = new abort_controller_1.default();
            reqTimeout = setTimeout(() => {
                controller.abort();
            }, options.timeout);
            fetchOptions.signal = controller.signal;
        }
        if (typeof options.body === "string" || isStream(options.body)) {
            fetchOptions.body = options.body;
        }
        else if (options.body !== undefined) {
            fetchOptions.body = JSON.stringify(options.body);
        }
        const operationOptions = {
            retries: ((_a = options.retryCodes) === null || _a === void 0 ? void 0 : _a.length) ? 1 : 2,
            minTimeout: 1 * 1000,
            maxTimeout: 5 * 1000,
        };
        if (typeof options.retries === "number") {
            operationOptions.retries = options.retries;
        }
        if (typeof options.retryMinTimeout === "number") {
            operationOptions.minTimeout = options.retryMinTimeout;
        }
        if (typeof options.retryMaxTimeout === "number") {
            operationOptions.maxTimeout = options.retryMaxTimeout;
        }
        const operation = retry.operation(operationOptions);
        return await new Promise((resolve, reject) => {
            operation.attempt(async (currentAttempt) => {
                var _a;
                let res;
                let body;
                try {
                    if (currentAttempt > 1) {
                        logger_1.logger.debug(`*** [apiv2] Attempting the request again. Attempt number ${currentAttempt}`);
                    }
                    this.logRequest(options);
                    try {
                        res = await (0, node_fetch_1.default)(fetchURL, fetchOptions);
                    }
                    catch (thrown) {
                        const err = thrown instanceof Error ? thrown : new Error(thrown);
                        logger_1.logger.debug(`*** [apiv2] error from fetch(${fetchURL}, ${JSON.stringify(fetchOptions)}): ${err}`);
                        const isAbortError = err.name.includes("AbortError");
                        if (isAbortError) {
                            throw new error_1.FirebaseError(`Timeout reached making request to ${fetchURL}`, {
                                original: err,
                            });
                        }
                        throw new error_1.FirebaseError(`Failed to make request to ${fetchURL}`, { original: err });
                    }
                    finally {
                        if (reqTimeout) {
                            clearTimeout(reqTimeout);
                        }
                    }
                    if (options.responseType === "json") {
                        const text = await res.text();
                        if (!text.length) {
                            body = undefined;
                        }
                        else {
                            try {
                                body = JSON.parse(text);
                            }
                            catch (err) {
                                this.logResponse(res, text, options);
                                throw new error_1.FirebaseError(`Unable to parse JSON: ${err}`);
                            }
                        }
                    }
                    else if (options.responseType === "xml") {
                        body = (await res.text());
                    }
                    else if (options.responseType === "stream") {
                        body = res.body;
                    }
                    else {
                        throw new error_1.FirebaseError(`Unable to interpret response. Please set responseType.`, {
                            exit: 2,
                        });
                    }
                }
                catch (err) {
                    return err instanceof error_1.FirebaseError ? reject(err) : reject(new error_1.FirebaseError(`${err}`));
                }
                this.logResponse(res, body, options);
                if (res.status >= 400) {
                    if ((_a = options.retryCodes) === null || _a === void 0 ? void 0 : _a.includes(res.status)) {
                        const err = (0, responseToError_1.responseToError)({ statusCode: res.status }, body) || undefined;
                        if (operation.retry(err)) {
                            return;
                        }
                    }
                    if (!options.resolveOnHTTPError) {
                        return reject((0, responseToError_1.responseToError)({ statusCode: res.status }, body));
                    }
                }
                resolve({
                    status: res.status,
                    response: res,
                    body,
                });
            });
        });
    }
    logRequest(options) {
        var _a, _b;
        let queryParamsLog = "[none]";
        if (options.queryParams) {
            queryParamsLog = "[omitted]";
            if (!((_a = options.skipLog) === null || _a === void 0 ? void 0 : _a.queryParams)) {
                queryParamsLog =
                    options.queryParams instanceof url_1.URLSearchParams
                        ? options.queryParams.toString()
                        : JSON.stringify(options.queryParams);
            }
        }
        const logURL = this.requestURL(options);
        logger_1.logger.debug(`>>> [apiv2][query] ${options.method} ${logURL} ${queryParamsLog}`);
        const headers = options.headers;
        if (headers && headers.has(GOOG_QUOTA_USER)) {
            logger_1.logger.debug(`>>> [apiv2][(partial)header] ${options.method} ${logURL} x-goog-quota-user=${headers.get(GOOG_QUOTA_USER) || ""}`);
        }
        if (options.body !== undefined) {
            let logBody = "[omitted]";
            if (!((_b = options.skipLog) === null || _b === void 0 ? void 0 : _b.body)) {
                logBody = bodyToString(options.body);
            }
            logger_1.logger.debug(`>>> [apiv2][body] ${options.method} ${logURL} ${logBody}`);
        }
    }
    logResponse(res, body, options) {
        var _a;
        const logURL = this.requestURL(options);
        logger_1.logger.debug(`<<< [apiv2][status] ${options.method} ${logURL} ${res.status}`);
        let logBody = "[omitted]";
        if (!((_a = options.skipLog) === null || _a === void 0 ? void 0 : _a.resBody)) {
            logBody = bodyToString(body);
        }
        logger_1.logger.debug(`<<< [apiv2][body] ${options.method} ${logURL} ${logBody}`);
    }
}
exports.Client = Client;
function isLocalInsecureRequest(urlPrefix) {
    const u = new url_1.URL(urlPrefix);
    return u.protocol === "http:";
}
function bodyToString(body) {
    if (isStream(body)) {
        return "[stream]";
    }
    else {
        try {
            return JSON.stringify(body);
        }
        catch (_) {
            return util_1.default.inspect(body);
        }
    }
}
function isStream(o) {
    return o instanceof stream_1.Readable || o instanceof FormData;
}
