// Copyright 2021-2025 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as http from "node:http";
import * as https from "node:https";
import { Code, ConnectError } from "@connectrpc/connect";
import { nodeHeaderToWebHeader, webHeaderToNodeHeaders, } from "./node-universal-header.js";
import { connectErrorFromH2ResetCode, connectErrorFromNodeReason, getNodeErrorProps, H2Code, unwrapNodeErrorChain, } from "./node-error.js";
import { getAbortSignalReason } from "@connectrpc/connect/protocol";
import { Http2SessionManager } from "./http2-session-manager.js";
/**
 * Create a universal client function, a minimal abstraction of an HTTP client,
 * using the Node.js `http`, `https`, or `http2` module.
 *
 * @private Internal code, does not follow semantic versioning.
 */
export function createNodeHttpClient(options) {
    var _a;
    if (options.httpVersion == "1.1") {
        return createNodeHttp1Client(options.nodeOptions);
    }
    const sessionProvider = (_a = options.sessionProvider) !== null && _a !== void 0 ? _a : ((url) => new Http2SessionManager(url));
    return createNodeHttp2Client(sessionProvider);
}
/**
 * Create an HTTP client using the Node.js `http` or `https` package.
 *
 * The HTTP client is a simple function conforming to the type UniversalClientFn.
 * It takes an UniversalClientRequest as an argument, and returns a promise for
 * an UniversalClientResponse.
 */
function createNodeHttp1Client(httpOptions) {
    return async function request(req) {
        const sentinel = createSentinel(req.signal);
        return new Promise((resolve, reject) => {
            sentinel.onError((e) => {
                reject(e);
            });
            h1Request(sentinel, req.url, Object.assign(Object.assign({}, httpOptions), { headers: webHeaderToNodeHeaders(req.header, httpOptions === null || httpOptions === void 0 ? void 0 : httpOptions.headers), method: req.method }), (request) => {
                void sinkRequest(req, request, sentinel);
                request.on("response", (response) => {
                    var _a;
                    response.on("error", sentinel.error);
                    sentinel.onError((reason) => response.destroy(reason));
                    const trailer = new Headers();
                    resolve({
                        status: (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0,
                        header: nodeHeaderToWebHeader(response.headers),
                        body: h1ResponseIterable(sentinel, response, trailer),
                        trailer,
                    });
                });
            });
        });
    };
}
/**
 * Create an HTTP client using the Node.js `http2` package.
 *
 * The HTTP client is a simple function conforming to the type UniversalClientFn.
 * It takes an UniversalClientRequest as an argument, and returns a promise for
 * an UniversalClientResponse.
 */
function createNodeHttp2Client(sessionProvider) {
    return function request(req) {
        const sentinel = createSentinel(req.signal);
        const sessionManager = sessionProvider(req.url);
        return new Promise((resolve, reject) => {
            sentinel.onError((e) => {
                reject(e);
            });
            h2Request(sentinel, sessionManager, req.url, req.method, webHeaderToNodeHeaders(req.header), {}, (stream) => {
                void sinkRequest(req, stream, sentinel);
                stream.on("response", (headers) => {
                    var _a;
                    const response = {
                        status: (_a = headers[":status"]) !== null && _a !== void 0 ? _a : 0,
                        header: nodeHeaderToWebHeader(headers),
                        body: h2ResponseIterable(sentinel, stream, sessionManager),
                        trailer: h2ResponseTrailer(stream),
                    };
                    resolve(response);
                });
            });
        });
    };
}
function h1Request(sentinel, url, options, onRequest) {
    let request;
    if (new URL(url).protocol.startsWith("https")) {
        request = https.request(url, options);
    }
    else {
        request = http.request(url, options);
    }
    sentinel.onError((reason) => request.destroy(reason));
    // Node.js will only send headers with the first request body byte by default.
    // We force it to send headers right away for consistent behavior between
    // HTTP/1.1 and HTTP/2.0 clients.
    request.flushHeaders();
    request.on("error", sentinel.error);
    request.on("socket", function onRequestSocket(socket) {
        function onSocketConnect() {
            socket.off("connect", onSocketConnect);
            onRequest(request);
        }
        // If readyState is open, then socket is already open due to keepAlive, so
        // the 'connect' event will never fire so call onRequest explicitly
        if (socket.readyState === "open") {
            onRequest(request);
        }
        else {
            socket.on("connect", onSocketConnect);
        }
    });
}
function h1ResponseIterable(sentinel, response, trailer) {
    const inner = response[Symbol.asyncIterator]();
    return {
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    const r = await sentinel.race(inner.next());
                    if (r.done === true) {
                        nodeHeaderToWebHeader(response.trailers).forEach((value, key) => {
                            trailer.set(key, value);
                        });
                        sentinel.close();
                    }
                    return r;
                },
                throw(e) {
                    sentinel.error(e);
                    throw e;
                },
            };
        },
    };
}
function h2Request(sentinel, sm, url, method, headers, options, onStream) {
    const requestUrl = new URL(url);
    if (requestUrl.origin !== sm.authority) {
        const message = `cannot make a request to ${requestUrl.origin}: the http2 session is connected to ${sm.authority}`;
        sentinel.error(new ConnectError(message, Code.Internal));
        return;
    }
    sm.request(method, requestUrl.pathname + requestUrl.search, headers, {}).then((stream) => {
        sentinel.onError((reason) => {
            if (stream.closed) {
                return;
            }
            // Node.js http2 streams that are aborted via an AbortSignal close with
            // an RST_STREAM with code INTERNAL_ERROR.
            // To comply with the mapping between gRPC and HTTP/2 codes, we need to
            // close with code CANCEL.
            // See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors
            // See https://www.rfc-editor.org/rfc/rfc7540#section-7
            const rstCode = reason.code == Code.Canceled ? H2Code.CANCEL : H2Code.INTERNAL_ERROR;
            return new Promise((resolve) => stream.close(rstCode, resolve));
        });
        stream.on("error", function h2StreamError(e) {
            if (stream.writableEnded &&
                unwrapNodeErrorChain(e)
                    .map(getNodeErrorProps)
                    .some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
                return;
            }
            sentinel.error(e);
        });
        stream.on("close", function h2StreamClose() {
            const err = connectErrorFromH2ResetCode(stream.rstCode);
            if (err) {
                sentinel.error(err);
            }
        });
        onStream(stream);
    }, (reason) => {
        sentinel.error(reason);
    });
}
function h2ResponseTrailer(response) {
    const trailer = new Headers();
    response.on("trailers", (args) => {
        nodeHeaderToWebHeader(args).forEach((value, key) => {
            trailer.set(key, value);
        });
    });
    return trailer;
}
function h2ResponseIterable(sentinel, response, sm) {
    const inner = response[Symbol.asyncIterator]();
    return {
        [Symbol.asyncIterator]() {
            return {
                async next() {
                    const r = await sentinel.race(inner.next());
                    if (r.done === true) {
                        sentinel.close();
                    }
                    sm === null || sm === void 0 ? void 0 : sm.notifyResponseByteRead(response);
                    return r;
                },
                throw(e) {
                    sentinel.error(e);
                    throw e;
                },
            };
        },
    };
}
async function sinkRequest(request, nodeRequest, sentinel) {
    if (request.body === undefined) {
        await new Promise((resolve) => nodeRequest.end(resolve));
        return;
    }
    const it = request.body[Symbol.asyncIterator]();
    return new Promise((resolve) => {
        writeNext();
        function writeNext() {
            if (sentinel.isClosed()) {
                return;
            }
            it.next().then((r) => {
                if (r.done === true) {
                    nodeRequest.end(resolve);
                    return;
                }
                nodeRequest.write(r.value, "binary", (e) => {
                    if (e === null || e === undefined) {
                        writeNext();
                        return;
                    }
                    if (it.throw !== undefined) {
                        it.throw(connectErrorFromNodeReason(e)).catch(() => {
                            //
                        });
                    }
                    // If the server responds and closes the connection before the client has written the entire response
                    // body, we get an ERR_STREAM_WRITE_AFTER_END error code from Node.js here.
                    // We do want to notify the iterable of the error condition, but we do not want to reject our sentinel,
                    // because that would also affect the reading side.
                    if (nodeRequest.writableEnded &&
                        unwrapNodeErrorChain(e)
                            .map(getNodeErrorProps)
                            .some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
                        return;
                    }
                    sentinel.error(e);
                });
            }, (e) => {
                sentinel.error(e);
            });
        }
    });
}
function createSentinel(signal) {
    let rejectRace;
    let closed = false;
    let closedError = undefined;
    let onErrorListeners = [];
    const sentinel = {
        error(error) {
            if (closed) {
                return;
            }
            closed = true;
            closedError =
                error instanceof ConnectError
                    ? error
                    : connectErrorFromNodeReason(error);
            rejectRace === null || rejectRace === void 0 ? void 0 : rejectRace(closedError);
            for (const onRejected of onErrorListeners) {
                onRejected(closedError);
            }
            cleanup();
        },
        close() {
            if (closed) {
                return;
            }
            closed = true;
            if (rejectRace) {
                rejectRace(new ConnectError("sentinel completed early", Code.Internal));
            }
            cleanup();
        },
        isClosed() {
            return closed;
        },
        onError(onError) {
            if (closed) {
                if (closedError !== undefined) {
                    onError(closedError);
                }
            }
            else {
                onErrorListeners.push(onError);
            }
        },
        race(promise) {
            let resolveRace;
            const race = new Promise((resolve, reject) => {
                resolveRace = resolve;
                rejectRace = reject;
            });
            promise.then((value) => {
                resolveRace === null || resolveRace === void 0 ? void 0 : resolveRace(value);
            }, (reason) => {
                rejectRace === null || rejectRace === void 0 ? void 0 : rejectRace(reason);
            });
            if (closed) {
                rejectRace === null || rejectRace === void 0 ? void 0 : rejectRace(closedError !== null && closedError !== void 0 ? closedError : new ConnectError("sentinel completed early", Code.Internal));
            }
            return race;
        },
    };
    function cleanup() {
        if (signal) {
            signal.removeEventListener("abort", onSignalAbort);
        }
        onErrorListeners = [];
        rejectRace = undefined;
    }
    function onSignalAbort() {
        sentinel.error(getAbortSignalReason(this));
    }
    if (signal) {
        signal.addEventListener("abort", onSignalAbort);
        if (signal.aborted) {
            sentinel.error(getAbortSignalReason(signal));
        }
    }
    return sentinel;
}
