"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoxNetworkClient = exports.shouldIncludeBoxUaHeader = exports.xBoxUaHeader = exports.userAgentHeader = void 0;
exports.getRetryTimeout = getRetryTimeout;
const node_fetch_1 = __importDefault(require("node-fetch"));
const errors_1 = require("../box/errors");
const utils_1 = require("../internal/utils");
const version_1 = require("./version");
const json_1 = require("../serialization/json");
const retries_1 = require("./retries");
const logging_1 = require("../internal/logging");
exports.userAgentHeader = `Box JavaScript generated SDK v${version_1.sdkVersion} (${(0, utils_1.isBrowser)() ? navigator.userAgent : `Node ${process.version}`})`;
exports.xBoxUaHeader = constructBoxUAHeader();
const shouldIncludeBoxUaHeader = (options) => {
    return !((0, utils_1.isBrowser)() &&
        (options.responseFormat === 'binary' ||
            options.responseFormat === 'no_content'));
};
exports.shouldIncludeBoxUaHeader = shouldIncludeBoxUaHeader;
async function createRequestInit(options) {
    const { method = 'GET', headers = {}, contentType: contentTypeInput = 'application/json', data, fileStream, } = options;
    const { contentHeaders = {}, body } = await (async () => {
        const contentHeaders = {};
        if (options.multipartData) {
            const FormDataClass = (0, utils_1.isBrowser)() ? window.FormData : utils_1.FormData;
            const formData = new FormDataClass();
            for (const item of options.multipartData) {
                if (item.fileStream) {
                    const buffer = await (0, utils_1.readByteStream)(item.fileStream);
                    const blob = (0, utils_1.isBrowser)()
                        ? new Blob([new Uint8Array(buffer)])
                        : buffer;
                    contentHeaders['content-md5'] = await (0, utils_1.calculateMD5Hash)(buffer);
                    formData.append(item.partName, blob, {
                        filename: item.fileName ?? 'file',
                        contentType: item.contentType ?? 'application/octet-stream',
                    });
                }
                else if (item.data) {
                    formData.append(item.partName, (0, json_1.sdToJson)(item.data));
                }
                else {
                    throw new errors_1.BoxSdkError({
                        message: 'Multipart item must have either body or fileStream',
                    });
                }
            }
            return {
                contentHeaders: {
                    ...(!(0, utils_1.isBrowser)() && {
                        'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
                    }),
                    ...contentHeaders,
                },
                body: formData,
            };
        }
        contentHeaders['Content-Type'] = contentTypeInput;
        switch (contentTypeInput) {
            case 'application/json':
            case 'application/json-patch+json':
                return { contentHeaders, body: (0, json_1.sdToJson)(data) };
            case 'application/x-www-form-urlencoded':
                return { contentHeaders, body: (0, json_1.sdToUrlParams)(data) };
            case 'application/octet-stream':
                if (!fileStream) {
                    throw new errors_1.BoxSdkError({
                        message: 'fileStream required for application/octet-stream content type',
                    });
                }
                return {
                    contentHeaders,
                    body: (0, utils_1.isBrowser)()
                        ? await (0, utils_1.readByteStream)(fileStream)
                        : fileStream,
                };
            default:
                throw new errors_1.BoxSdkError({
                    message: `Unsupported content type : ${contentTypeInput}`,
                });
        }
    })();
    return {
        method,
        headers: {
            // Only set content type if it is not a GET request
            ...(method != 'GET' && contentHeaders),
            ...headers,
            ...(options.auth && {
                Authorization: await options.auth.retrieveAuthorizationHeader(options.networkSession),
            }),
            ...((0, exports.shouldIncludeBoxUaHeader)(options) && {
                'User-Agent': exports.userAgentHeader,
                'X-Box-UA': exports.xBoxUaHeader,
            }),
            // Additional headers will override the default headers
            ...options.networkSession?.additionalHeaders,
        },
        body: body,
        signal: options.cancellationToken,
        agent: options.networkSession?.agent,
        ...(fileStream && (0, utils_1.isBrowser)() && { duplex: 'half' }),
    };
}
class BoxNetworkClient {
    constructor(fields) {
        Object.assign(this, fields);
    }
    async fetch(options) {
        const attemptNumber = options.attemptNumber ?? 1;
        let numberOfRetriesOnException = options.numberOfRetriesOnException ?? 0;
        const interceptors = options.networkSession?.interceptors ?? [];
        const retryStrategy = options.networkSession?.retryStrategy ?? new retries_1.BoxRetryStrategy({});
        const dataSanitizer = options.networkSession?.dataSanitizer ?? new logging_1.DataSanitizer({});
        const fetchOptions = interceptors.length
            ? interceptors.reduce((modifiedOptions, interceptor) => interceptor.beforeRequest(modifiedOptions), options)
            : options;
        const fileStreamBuffer = fetchOptions.fileStream
            ? await (0, utils_1.readByteStream)(fetchOptions.fileStream)
            : void 0;
        const multipartBuffer = fetchOptions.multipartData
            ? await (0, utils_1.multipartStreamToBuffer)(fetchOptions.multipartData)
            : void 0;
        let isExceptionCase = false;
        let fetchResponse;
        let responseBytesBuffer;
        let caughtError;
        const { params = {} } = fetchOptions;
        const requestInit = await createRequestInit({
            ...fetchOptions,
            fileStream: fileStreamBuffer
                ? (0, utils_1.generateByteStreamFromBuffer)(fileStreamBuffer)
                : void 0,
            multipartData: multipartBuffer
                ? (0, utils_1.multipartBufferToStream)(multipartBuffer)
                : void 0,
        });
        try {
            const response = await (0, node_fetch_1.default)(''.concat(fetchOptions.url, Object.keys(params).length === 0 || fetchOptions.url.endsWith('?')
                ? ''
                : '?', new URLSearchParams(params).toString()), { ...requestInit, redirect: (0, utils_1.isBrowser)() ? 'follow' : 'manual' });
            const contentType = response.headers.get('content-type') ?? '';
            const ignoreResponseBody = fetchOptions.followRedirects === false;
            let data;
            let content = (0, utils_1.generateByteStreamFromBuffer)(new Uint8Array().buffer);
            if (!ignoreResponseBody) {
                if (options.responseFormat === 'binary') {
                    content = response.body;
                    responseBytesBuffer = new Uint8Array();
                }
                else if (options.responseFormat === 'json') {
                    responseBytesBuffer = await response.arrayBuffer();
                    const text = new TextDecoder().decode(responseBytesBuffer);
                    if (text) {
                        data = (0, json_1.jsonToSerializedData)(text);
                    }
                    content = (0, utils_1.generateByteStreamFromBuffer)(responseBytesBuffer);
                }
            }
            fetchResponse = {
                url: response.url,
                status: response.status,
                data,
                content,
                headers: Object.fromEntries(Array.from(response.headers.entries())),
            };
            if (interceptors.length) {
                fetchResponse = interceptors.reduce((modifiedResponse, interceptor) => interceptor.afterRequest(modifiedResponse), fetchResponse);
            }
        }
        catch (error) {
            isExceptionCase = true;
            numberOfRetriesOnException++;
            caughtError = error instanceof Error ? error : new Error(String(error));
            fetchResponse = fetchResponse ?? { status: 0, headers: {} };
        }
        const attemptForRetry = isExceptionCase
            ? numberOfRetriesOnException
            : attemptNumber;
        const shouldRetry = await retryStrategy.shouldRetry(fetchOptions, fetchResponse, attemptForRetry);
        if (shouldRetry) {
            const retryTimeout = retryStrategy.retryAfter(fetchOptions, fetchResponse, attemptForRetry);
            await new Promise((resolve) => setTimeout(resolve, retryTimeout));
            return this.fetch({
                ...options,
                attemptNumber: attemptNumber + 1,
                numberOfRetriesOnException: numberOfRetriesOnException,
                fileStream: fileStreamBuffer
                    ? (0, utils_1.generateByteStreamFromBuffer)(fileStreamBuffer)
                    : void 0,
                multipartData: multipartBuffer
                    ? (0, utils_1.multipartBufferToStream)(multipartBuffer)
                    : void 0,
            });
        }
        if (fetchResponse.status >= 300 &&
            fetchResponse.status < 400 &&
            fetchOptions.followRedirects !== false) {
            if (!fetchResponse.headers['location']) {
                throw new errors_1.BoxSdkError({
                    message: `Unable to follow redirect for ${fetchOptions.url}`,
                });
            }
            const sameOrigin = new URL(fetchResponse.headers['location']).origin ===
                new URL(fetchOptions.url).origin;
            return this.fetch({
                ...options,
                params: undefined,
                auth: sameOrigin ? fetchOptions.auth : undefined,
                url: fetchResponse.headers['location'],
            });
        }
        if (fetchResponse.status >= 200 && fetchResponse.status < 400) {
            return fetchResponse;
        }
        const [code, contextInfo, requestId, helpUrl, message, error, errorDescription,] = (0, json_1.sdIsMap)(fetchResponse.data)
            ? [
                (0, json_1.sdToJson)(fetchResponse.data['code']),
                (0, json_1.sdIsMap)(fetchResponse.data['context_info'])
                    ? fetchResponse.data['context_info']
                    : undefined,
                (0, json_1.sdToJson)(fetchResponse.data['request_id']),
                (0, json_1.sdToJson)(fetchResponse.data['help_url']),
                (0, json_1.sdToJson)(fetchResponse.data['message']),
                (0, json_1.sdToJson)(fetchResponse.data['error']),
                (0, json_1.sdToJson)(fetchResponse.data['error_description']),
            ]
            : [];
        if (fetchResponse.status === 0) {
            throw new errors_1.BoxSdkError({
                message: `Unexpected Error occurred`,
                timestamp: `${Date.now()}`,
                error: caughtError,
            });
        }
        const errorMessage = [
            [fetchResponse.status, code, message].filter(Boolean).join(' '),
            requestId && `Request ID: ${requestId}`,
            error && `${error} - ${errorDescription}`,
        ]
            .filter(Boolean)
            .join('; ');
        throw new errors_1.BoxApiError({
            message: errorMessage,
            timestamp: `${Date.now()}`,
            requestInfo: {
                method: requestInit.method,
                url: fetchOptions.url,
                queryParams: params,
                headers: requestInit.headers ?? {},
                body: typeof requestInit.body === 'string' ? requestInit.body : undefined,
            },
            responseInfo: {
                statusCode: fetchResponse.status,
                headers: fetchResponse.headers,
                body: fetchResponse.data,
                rawBody: new TextDecoder().decode(responseBytesBuffer),
                code: code,
                contextInfo: contextInfo,
                requestId: requestId,
                helpUrl: helpUrl,
            },
            dataSanitizer: dataSanitizer,
        });
    }
}
exports.BoxNetworkClient = BoxNetworkClient;
function constructBoxUAHeader() {
    const analyticsIdentifiers = {
        agent: `box-javascript-generated-sdk/${version_1.sdkVersion}`,
        env: (0, utils_1.isBrowser)()
            ? navigator.userAgent
            : `Node/${process.version.replace('v', '')}`,
    };
    return Object.keys(analyticsIdentifiers)
        .map((k) => `${k}=${analyticsIdentifiers[k]}`)
        .join('; ');
}
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
const RETRY_RANDOMIZATION_FACTOR = 0.5;
/**
 * Calculate the exponential backoff time with randomized jitter
 * @param {int} numRetries Which retry number this one will be
 * @param {int} baseInterval The base retry interval set in config
 * @returns {int} The number of milliseconds after which to retry
 */
function getRetryTimeout(numRetries, baseInterval) {
    var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
    var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
    var randomization = Math.random() * (maxRandomization - minRandomization) + minRandomization;
    var exponential = Math.pow(2, numRetries - 1);
    return Math.ceil(exponential * baseInterval * randomization);
}
//# sourceMappingURL=boxNetworkClient.js.map