"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasicApiClient = void 0;
const ApiError_1 = require("./ApiError");
const axiosProxyConfig_1 = require("../http/axiosProxyConfig");
const axios_1 = __importDefault(require("axios"));
const os = __importStar(require("os"));
const async_retry_1 = __importDefault(require("async-retry"));
const logUtils_1 = require("../util/logUtils");
const asyncUtil_1 = require("../util/asyncUtil");
const types_1 = require("./types");
const httpUtil_1 = require("../http/httpUtil");
const cliConfigProvider_1 = require("../providers/cliConfigProvider");
const MABL_ENTITY_VERSION_HEADER = 'x-mabl-entity-version';
const DEFAULT_RETRYABLE_REQUEST_TIMEOUT_MILLISECONDS = 60_000;
const DEFAULT_RETRIES = 10;
const DEFAULT_MAX_TOTAL_RETRY_TIME_MILLISECONDS = DEFAULT_RETRYABLE_REQUEST_TIMEOUT_MILLISECONDS * DEFAULT_RETRIES;
const DEFAULT_NONRETRYABLE_REQUEST_TIMEOUT_MILLISECONDS = DEFAULT_MAX_TOTAL_RETRY_TIME_MILLISECONDS;
const DEFAULT_MIN_RETRY_INTERVAL_MILLISECONDS = 1_000;
const DEFAULT_MAX_RETRY_INTERVAL_MILLISECONDS = 10_000;
const RETRYABLE_NODEJS_ERRORS = [
    'EAI_AGAIN',
    'ECONNRESET',
    'ENOTFOUND',
    'ENETUNREACH',
    'ETIMEOUT',
    'ETIMEDOUT',
    'ECONNABORTED',
];
const DEFAULT_SSL_VERIFY = false;
class BasicApiClient {
    static async create() {
        const httpConfig = (await cliConfigProvider_1.CliConfigProvider.getCliConfig()).http.mabl;
        return new BasicApiClient({
            authType: types_1.AuthType.None,
            token: '',
            proxyUrl: httpConfig.proxyHost,
            sslVerify: httpConfig.sslVerify,
            proxyType: httpConfig.proxyType,
        });
    }
    constructor(options) {
        const config = (0, axiosProxyConfig_1.axiosProxyConfig)({
            sslVerify: options.sslVerify ?? DEFAULT_SSL_VERIFY,
            proxyHost: options.proxyUrl,
            proxyType: options.proxyType,
        });
        if (!config.headers) {
            config.headers = {};
        }
        config.timeout =
            options.requestTimeoutMillis ??
                options.retryConfig?.requestTimeoutMillis ??
                DEFAULT_RETRYABLE_REQUEST_TIMEOUT_MILLISECONDS;
        config.maxBodyLength = Infinity;
        config.maxContentLength = Infinity;
        switch (options.authType) {
            case types_1.AuthType.ApiKey:
                if (!options.token) {
                    throw new Error(`Auth type [${options.authType}] requires token`);
                }
                config.auth = {
                    username: 'key',
                    password: options.token,
                };
                break;
            case types_1.AuthType.Basic:
                if (!options.token) {
                    throw new Error(`Auth type [${options.authType}] requires token`);
                }
                const parts = options.token.split(':');
                if (parts.length !== 2) {
                    throw new Error('Invalid basic auth token');
                }
                const [username, password] = parts;
                config.auth = {
                    username,
                    password,
                };
                break;
            case types_1.AuthType.Bearer:
                if (!options.token && !options.tokenAccessor) {
                    throw new Error(`Auth type [${options.authType}] requires token or tokenAccessor`);
                }
                if (options.token) {
                    config.headers.authorization = `Bearer ${options.token}`;
                }
                break;
            case types_1.AuthType.None:
                break;
            default:
                throw new Error(`Unhandled auth type [${options.authType}`);
        }
        config.headers = {
            ...config.headers,
            ...{
                Accept: 'application/json',
                'x-mabl-user-id': 'mabl-cli',
                'x-mabl-user-platform': `${os.platform()}-${os.release()}`,
            },
        };
        if (options.userAgentOverride) {
            config.headers[httpUtil_1.USER_AGENT_HEADER] = options.userAgentOverride;
        }
        this.httpRequestConfig = config;
        this.httpClient = axios_1.default.create(config);
        this.httpClient.defaults.headers.common = { ...config.headers };
        const { tokenAccessor } = options;
        if (tokenAccessor) {
            this.httpClient.interceptors.request.use(async (config) => {
                const token = await tokenAccessor();
                if (token) {
                    config.headers.set('authorization', `Bearer ${token}`);
                }
                return config;
            });
        }
        this.retryConfig = options.retryConfig;
        this.debugLogger = options.debugLogger ?? logUtils_1.logInternal;
    }
    getNonRetryableRequestConfig(override) {
        const overrideWithTimeout = { ...(override ?? {}) };
        if (!overrideWithTimeout.timeout) {
            overrideWithTimeout.timeout =
                DEFAULT_NONRETRYABLE_REQUEST_TIMEOUT_MILLISECONDS;
        }
        return { ...this.httpRequestConfig, ...overrideWithTimeout };
    }
    getRetryableRequestConfig(retryConfig) {
        return this.getNonRetryableRequestConfig({
            timeout: retryConfig?.requestTimeoutMillis ?? this.httpRequestConfig.timeout,
        });
    }
    makeGetRequest(path, retryConfig, axiosConfig) {
        return this.retryWrappedRequest(`makeGetRequest('${path}')`, () => this.getRequest(path, {
            ...axiosConfig,
            ...this.getRetryableRequestConfig(retryConfig),
        }), retryConfig);
    }
    async getRequest(path, config) {
        const response = await this.debugRequest('GET', path, () => this.httpClient.get(path, config));
        BasicApiClient.checkResponseStatusCode(response);
        return response.data;
    }
    makeGetRequestWithETag(path, retryConfig) {
        return this.retryWrappedRequest(`makeGetRequestWithETag('${path}')`, () => this.getRequestWithETag(path, this.getRetryableRequestConfig(retryConfig)), retryConfig);
    }
    async getRequestWithETag(path, config) {
        const response = await this.debugRequest('GET', path, () => this.httpClient.get(path, config));
        BasicApiClient.checkResponseStatusCode(response);
        const headers = response.headers;
        const result = response.data;
        const versionHeader = headers[MABL_ENTITY_VERSION_HEADER]?.toString();
        if (!versionHeader) {
            throw new Error(`Missing ${MABL_ENTITY_VERSION_HEADER} header`);
        }
        return { ...result, entity_version: versionHeader };
    }
    async makePostRequest(path, requestBody, requestConfig) {
        return this.postRequest(path, requestBody, this.getNonRetryableRequestConfig(requestConfig));
    }
    async makePostRequestWithRetries(path, requestBody, config) {
        return this.retryWrappedRequest(`makePostRequestWithRetries('${path}')`, () => this.postRequest(path, requestBody, this.getRetryableRequestConfig(config)), config);
    }
    async postRequest(path, requestBody, config) {
        const response = await this.debugRequest('POST', path, () => this.httpClient.post(path, requestBody, config));
        BasicApiClient.checkResponseStatusCode(response);
        return response.data;
    }
    async makePutRequest(path, requestBody) {
        const response = await this.debugRequest('PUT', path, () => this.httpClient.put(path, requestBody, this.getNonRetryableRequestConfig()));
        BasicApiClient.checkResponseStatusCode(response);
        return response.data;
    }
    makeDeleteRequest(path, retryConfig) {
        return this.retryWrappedRequest(`makeDeleteRequest('${path}')`, () => this.deleteRequest(path, this.getRetryableRequestConfig(retryConfig)), retryConfig);
    }
    async deleteRequest(path, config) {
        const response = await this.debugRequest('DELETE', path, () => this.httpClient.delete(path, config));
        BasicApiClient.checkResponseStatusCode(response);
        return response.data;
    }
    async makePatchRequest(path, requestBody, ifMatch, requestConfig) {
        return this.patchRequest(path, requestBody, ifMatch, this.getNonRetryableRequestConfig(requestConfig));
    }
    async makePatchRequestWithRetries(path, requestBody, ifMatch, config) {
        return this.retryWrappedRequest(`makePatchRequestWithRetries('${path}')`, () => this.patchRequest(path, requestBody, ifMatch, this.getRetryableRequestConfig(config)), config);
    }
    async patchRequest(path, requestBody, ifMatch, config) {
        const extraConfig = ifMatch ? { headers: { 'If-Match': ifMatch } } : {};
        const response = await this.debugRequest('PATCH', path, () => this.httpClient.patch(path, requestBody, { ...config, ...extraConfig }));
        BasicApiClient.checkResponseStatusCode(response);
        return response.data;
    }
    async debugRequest(method, path, request) {
        const startTimeMillis = Date.now();
        let responseCode;
        let error;
        try {
            this.debugLogger(`API Client: Sending ${method} ${path}`);
            const response = await request();
            responseCode = response.status;
            return response;
        }
        catch (e) {
            error = e;
            throw e;
        }
        finally {
            this.debugLogger(`API Client: ${method} ${path} ${error ? 'failed' : 'completed'} in ${Date.now() - startTimeMillis}ms with ${responseCode ?? error}`);
        }
    }
    static checkResponseStatusCode(response) {
        const statusCode = response.status;
        if (!statusCode || statusCode >= 400) {
            throw new ApiError_1.ApiError(`[${statusCode} - ${response.statusText}]`, statusCode);
        }
    }
    retryWrappedRequest(description, requestFunc, retryConfigOverride) {
        const retryOptions = {
            retries: retryConfigOverride?.retryCount ??
                this.retryConfig?.retryCount ??
                DEFAULT_RETRIES,
            minTimeout: retryConfigOverride?.minRetryIntervalMillis ??
                this.retryConfig?.minRetryIntervalMillis ??
                DEFAULT_MIN_RETRY_INTERVAL_MILLISECONDS,
            maxTimeout: retryConfigOverride?.maxRetryIntervalMillis ??
                this.retryConfig?.maxRetryIntervalMillis ??
                DEFAULT_MAX_RETRY_INTERVAL_MILLISECONDS,
            maxRetryTime: retryConfigOverride?.maxRetryTimeMillis ??
                this.retryConfig?.maxRetryTimeMillis ??
                DEFAULT_MAX_TOTAL_RETRY_TIME_MILLISECONDS,
            onRetry: (error) => {
                this.debugLogger(`Retrying failed API request "${description}"`, error);
            },
            forever: false,
        };
        return (0, asyncUtil_1.promiseWithTimeout)((0, async_retry_1.default)(async (bail) => {
            try {
                return await requestFunc();
            }
            catch (error) {
                if (axios_1.default.isAxiosError(error) && BasicApiClient.isRetryable(error)) {
                    throw error;
                }
                else {
                    let code;
                    if (axios_1.default.isAxiosError(error)) {
                        code = error.code;
                    }
                    this.debugLogger(`Non-retryable error on API client: ${code} ${description} ${error}`);
                    bail(error);
                    return {};
                }
            }
        }, retryOptions), retryOptions.maxRetryTime + retryOptions.maxTimeout, 'Retryable API request');
    }
    static isRetryable(axiosError) {
        if (axiosError.response) {
            const statusCode = axiosError.response.status;
            return (statusCode === 422 ||
                statusCode === 429 ||
                statusCode === 502 ||
                statusCode === 503 ||
                statusCode === 504);
        }
        return (axiosError.code !== undefined &&
            RETRYABLE_NODEJS_ERRORS.includes(axiosError.code));
    }
}
exports.BasicApiClient = BasicApiClient;
