/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module RpcInterface
 */
import { BentleyError, Logger } from "@itwin/core-bentley";
import { CommonLoggerCategory, RpcContentType, RpcMultipart, RpcProtocol, RpcRequestStatus, RpcResponseCacheControl, WEB_RPC_CONSTANTS, WebAppRpcRequest, } from "@itwin/core-common";
import { Stream } from "node:stream";
import { promisify } from "node:util";
import { brotliCompress, createBrotliCompress, createGzip, gzip, constants as zlibConstants } from "node:zlib";
/* eslint-disable @typescript-eslint/no-deprecated */
function configureResponse(protocol, request, fulfillment, res) {
    const success = protocol.getStatus(fulfillment.status) === RpcRequestStatus.Resolved;
    // TODO: Use stale-while-revalidate in cache headers. This needs to be tested, and does not currently have support in the router/caching-service.
    // This will allow browsers to use stale cached responses while also revalidating with the router, allowing us to start up a backend if necessary.
    // RPC Caching Service uses the s-maxage header to determine the TTL for the redis cache.
    const oneHourInSeconds = 3600;
    if (success && request.caching === RpcResponseCacheControl.Immutable) {
        // If response size is > 50 MB, do not cache it.
        if (fulfillment.result.objects.length > (50 * 10 ** 7)) {
            res.set("Cache-Control", "no-store");
        }
        else if (request.operation.operationName === "generateTileContent") {
            res.set("Cache-Control", "no-store");
        }
        else if (request.operation.operationName === "getConnectionProps") {
            // GetConnectionprops can't be cached on the browser longer than the lifespan of the backend. The lifespan of backend may shrink too. Keep it at 1 second to be safe.
            res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 24}, max-age=1, immutable`);
        }
        else if (request.operation.operationName === "getTileCacheContainerUrl") {
            // getTileCacheContainerUrl returns a SAS with an expiry of 23:59:59. We can't exceed that time when setting the max-age.
            res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 23}, max-age=${oneHourInSeconds * 23}, immutable`);
        }
        else {
            res.set("Cache-Control", `s-maxage=${oneHourInSeconds * 24}, max-age=${oneHourInSeconds * 48}, immutable`);
        }
    }
    if (fulfillment.retry) {
        res.set("Retry-After", fulfillment.retry);
    }
}
function configureText(fulfillment, res) {
    res.set(WEB_RPC_CONSTANTS.CONTENT, WEB_RPC_CONSTANTS.TEXT);
    return (fulfillment.status === 204) ? "" : fulfillment.result.objects;
}
function configureBinary(fulfillment, res) {
    res.set(WEB_RPC_CONSTANTS.CONTENT, WEB_RPC_CONSTANTS.BINARY);
    const data = fulfillment.result.data[0];
    return Buffer.isBuffer(data) ? data : Buffer.from(data);
}
function configureMultipart(fulfillment, res) {
    const response = RpcMultipart.createStream(fulfillment.result);
    const headers = response.getHeaders();
    for (const header in headers) {
        if (headers.hasOwnProperty(header)) {
            res.set(header, headers[header]);
        }
    }
    return response;
}
function configureStream(fulfillment) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    return fulfillment.result.stream;
}
async function configureEncoding(req, res, responseBody) {
    const acceptedEncodings = req.header("Accept-Encoding")?.split(",").map((value) => value.trim());
    if (!acceptedEncodings)
        return responseBody;
    const encoding = acceptedEncodings.includes("br") ? "br" : acceptedEncodings.includes("gzip") ? "gzip" : undefined;
    if (!encoding)
        return responseBody;
    res.set("Content-Encoding", encoding);
    const brotliOptions = {
        params: {
            // Experimentation revealed that the default compression quality significantly increases the compression time for larger texts.
            // Reducing the quality improves speed substantially without a significant loss in the compression ratio.
            [zlibConstants.BROTLI_PARAM_QUALITY]: 3,
        },
    };
    if (responseBody instanceof Stream) {
        const compressStream = encoding === "br" ? createBrotliCompress(brotliOptions) : createGzip();
        return responseBody.pipe(compressStream);
    }
    return encoding === "br" ? promisify(brotliCompress)(responseBody, brotliOptions) : promisify(gzip)(responseBody);
}
/** @internal */
export async function sendResponse(protocol, request, fulfillment, req, res) {
    logResponse(request, fulfillment.status, fulfillment.rawResult);
    const versionHeader = protocol.protocolVersionHeaderName;
    if (versionHeader && RpcProtocol.protocolVersion) {
        res.set(versionHeader, RpcProtocol.protocolVersion.toString());
    }
    const transportType = WebAppRpcRequest.computeTransportType(fulfillment.result, fulfillment.rawResult);
    let responseBody;
    if (transportType === RpcContentType.Binary) {
        responseBody = configureBinary(fulfillment, res);
    }
    else if (transportType === RpcContentType.Multipart) {
        responseBody = configureMultipart(fulfillment, res);
    }
    else if (transportType === RpcContentType.Stream) {
        responseBody = configureStream(fulfillment);
    }
    else {
        responseBody = configureText(fulfillment, res);
    }
    configureResponse(protocol, request, fulfillment, res);
    res.status(fulfillment.status);
    if (fulfillment.allowCompression)
        responseBody = await configureEncoding(req, res, responseBody);
    // This check should in theory look for instances of Readable, but that would break backend implementation at
    // core/backend/src/RpcBackend.ts
    if (responseBody instanceof Stream) {
        responseBody.pipe(res);
    }
    else {
        res.send(responseBody);
    }
}
function logResponse(request, statusCode, resultObj) {
    const metadata = {
        ActivityId: request.id, // eslint-disable-line @typescript-eslint/naming-convention
        method: request.method,
        path: request.path,
        operation: request.operation,
        statusCode,
        errorObj: resultObj instanceof Error ? BentleyError.getErrorProps(resultObj) : undefined,
    };
    if (statusCode < 400)
        Logger.logInfo(CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP success response", metadata);
    else
        Logger.logError(CommonLoggerCategory.RpcInterfaceBackend, "RPC over HTTP failure response", metadata);
}
//# sourceMappingURL=response.js.map