// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { createPipelineRequest, createHttpHeaders } from "@azure/core-rest-pipeline";
import { isReadRequest, prepareURL } from "../common/index.js";
import { Constants, ResourceType } from "../common/constants.js";
import { executePlugins, PluginOn } from "../plugins/Plugin.js";
import * as RetryUtility from "../retry/retryUtility.js";
import { defaultHttpAgent, defaultHttpsAgent } from "./defaultAgent.js";
import { ErrorResponse } from "./ErrorResponse.js";
import { bodyFromData } from "./request.js";
import { TimeoutError } from "./TimeoutError.js";
import { getCachedDefaultHttpClient } from "../utils/cachedClient.js";
import { createClientLogger } from "@azure/logger";
import { DiagnosticNodeType } from "../diagnostics/DiagnosticNodeInternal.js";
import { addDiagnosticChild } from "../utils/diagnostics.js";
import { getCurrentTimestampInMs } from "../utils/time.js";
const logger = createClientLogger("RequestHandler");
async function executeRequest(diagnosticNode, requestContext) {
    return executePlugins(diagnosticNode, requestContext, httpRequest, PluginOn.request);
}
/**
 * @hidden
 */
async function httpRequest(requestContext, diagnosticNode) {
    const controller = new AbortController();
    const signal = controller.signal;
    // Wrap users passed abort events and call our own internal abort()
    const userSignal = requestContext.options && requestContext.options.abortSignal;
    let userSignalListener = undefined;
    if (userSignal) {
        if (userSignal.aborted) {
            controller.abort();
        }
        else {
            userSignalListener = () => {
                controller.abort();
            };
            userSignal.addEventListener("abort", userSignalListener);
        }
    }
    let requestTimeout = requestContext.connectionPolicy.requestTimeout;
    // If the request is a read request and partition level failover or circuit breaker is enabled,
    // set a shorter timeout to allow for quicker failover in case of partition unavailability.
    // This is to ensure that read requests can quickly failover to another partition if the current one is unavailable.
    if ((requestContext.globalPartitionEndpointManager?.isPartitionLevelAutomaticFailoverEnabled() ||
        requestContext.globalPartitionEndpointManager?.isPartitionLevelCircuitBreakerEnabled()) &&
        requestContext.partitionKeyRangeId &&
        requestContext.resourceType === ResourceType.item &&
        isReadRequest(requestContext.operationType)) {
        requestTimeout = Math.min(requestContext.connectionPolicy.requestTimeout, Constants.RequestTimeoutForReadsInMs);
    }
    const timeout = setTimeout(() => {
        controller.abort();
    }, requestTimeout);
    let response;
    if (requestContext.body) {
        requestContext.body = bodyFromData(requestContext.body);
    }
    const httpsClient = requestContext.httpClient ?? getCachedDefaultHttpClient();
    const url = prepareURL(requestContext.endpoint, requestContext.path);
    const reqHeaders = createHttpHeaders(requestContext.headers);
    const pipelineRequest = createPipelineRequest({
        url,
        headers: reqHeaders,
        method: requestContext.method,
        abortSignal: signal,
        body: requestContext.body,
    });
    if (requestContext.requestAgent) {
        pipelineRequest.agent = requestContext.requestAgent;
    }
    else {
        const parsedUrl = new URL(url);
        pipelineRequest.agent = parsedUrl.protocol === "http:" ? defaultHttpAgent : defaultHttpsAgent;
        pipelineRequest.allowInsecureConnection = parsedUrl.protocol === "http:";
    }
    const startTimeUTCInMs = getCurrentTimestampInMs();
    try {
        if (requestContext.pipeline) {
            response = await requestContext.pipeline.sendRequest(httpsClient, pipelineRequest);
        }
        else {
            response = await httpsClient.sendRequest(pipelineRequest);
        }
    }
    catch (error) {
        if (error.name === "AbortError") {
            // If the user passed signal caused the abort, cancel the timeout and rethrow the error
            if (userSignal && userSignal.aborted === true) {
                throw error;
            }
            throw new TimeoutError(`Timeout Error! Request took more than ${requestContext.connectionPolicy.requestTimeout} ms`);
        }
        throw error;
    }
    finally {
        clearTimeout(timeout);
        if (userSignal && userSignalListener) {
            userSignal.removeEventListener("abort", userSignalListener);
        }
    }
    const result = response.status === 204 || response.status === 304 || response.bodyAsText === ""
        ? null
        : JSON.parse(response.bodyAsText);
    const responseHeaders = response.headers.toJSON();
    const substatus = responseHeaders[Constants.HttpHeaders.SubStatus]
        ? parseInt(responseHeaders[Constants.HttpHeaders.SubStatus], 10)
        : undefined;
    diagnosticNode.recordSuccessfulNetworkCall(startTimeUTCInMs, requestContext, response, substatus, url);
    if (response.status >= 400) {
        const errorResponse = new ErrorResponse(result.message);
        logger.warning(response.status +
            " " +
            requestContext.endpoint +
            " " +
            requestContext.path +
            " " +
            result.message);
        errorResponse.code = response.status;
        errorResponse.body = result;
        errorResponse.headers = responseHeaders;
        if (Constants.HttpHeaders.ActivityId in responseHeaders) {
            errorResponse.activityId = responseHeaders[Constants.HttpHeaders.ActivityId];
        }
        if (Constants.HttpHeaders.SubStatus in responseHeaders) {
            errorResponse.substatus = substatus;
        }
        if (Constants.HttpHeaders.RetryAfterInMs in responseHeaders) {
            errorResponse.retryAfterInMs = parseInt(responseHeaders[Constants.HttpHeaders.RetryAfterInMs], 10);
            Object.defineProperty(errorResponse, "retryAfterInMilliseconds", {
                get: () => {
                    return errorResponse.retryAfterInMs;
                },
            });
        }
        throw errorResponse;
    }
    return {
        headers: responseHeaders,
        result,
        code: response.status,
        substatus,
    };
}
/**
 * @hidden
 */
async function request(requestContext, diagnosticNode) {
    if (requestContext.body) {
        requestContext.body = bodyFromData(requestContext.body);
        if (!requestContext.body) {
            throw new Error("parameter data must be a javascript object, string, or Buffer");
        }
    }
    return addDiagnosticChild(async (childNode) => {
        return RetryUtility.execute({
            diagnosticNode: childNode,
            requestContext,
            executeRequest,
        });
    }, diagnosticNode, DiagnosticNodeType.REQUEST_ATTEMPTS);
}
export const RequestHandler = {
    request,
};
//# sourceMappingURL=RequestHandler.js.map