/**
 * Copyright (c) Microsoft Corporation.
 *
 * 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 dns from 'dns';
import http from 'http';
import https from 'https';
import net from 'net';
import tls from 'tls';
import { assert } from '@isomorphic/assert';
import { ManualPromise } from '@isomorphic/manualPromise';
import { monotonicTime } from '@isomorphic/time';
// Implementation(partial) of Happy Eyeballs 2 algorithm described in
// https://www.rfc-editor.org/rfc/rfc8305
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
const connectionAttemptDelayMs = 300;
const kDNSLookupAt = Symbol('kDNSLookupAt');
const kTCPConnectionAt = Symbol('kTCPConnectionAt');
class HttpHappyEyeballsAgent extends http.Agent {
    createConnection(options, oncreate) {
        // There is no ambiguity in case of IP address.
        if (net.isIP(clientRequestArgsToHostName(options)))
            return net.createConnection(options);
        createConnectionAsync(options, oncreate, /* useTLS */ false).catch(err => oncreate?.(err));
    }
}
class HttpsHappyEyeballsAgent extends https.Agent {
    createConnection(options, oncreate) {
        // There is no ambiguity in case of IP address.
        if (net.isIP(clientRequestArgsToHostName(options)))
            return tls.connect(options);
        createConnectionAsync(options, oncreate, /* useTLS */ true).catch(err => oncreate?.(err));
    }
}
// These options are aligned with the default Node.js globalAgent options.
export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent({ keepAlive: true });
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent({ keepAlive: true });
export async function createSocket(host, port) {
    return new Promise((resolve, reject) => {
        if (net.isIP(host)) {
            const socket = net.createConnection({ host, port });
            socket.on('connect', () => resolve(socket));
            socket.on('error', error => reject(error));
        }
        else {
            createConnectionAsync({ host, port }, (err, socket) => {
                if (err)
                    reject(err);
                if (socket)
                    resolve(socket);
            }, /* useTLS */ false).catch(err => reject(err));
        }
    });
}
export async function createTLSSocket(options) {
    return new Promise((resolve, reject) => {
        assert(options.host, 'host is required');
        if (net.isIP(options.host)) {
            const socket = tls.connect(options);
            socket.on('secureConnect', () => resolve(socket));
            socket.on('error', error => reject(error));
        }
        else {
            createConnectionAsync(options, (err, socket) => {
                if (err)
                    reject(err);
                if (socket) {
                    socket.on('secureConnect', () => resolve(socket));
                    socket.on('error', error => reject(error));
                }
            }, true).catch(err => reject(err));
        }
    });
}
export async function createConnectionAsync(options, oncreate, useTLS) {
    const lookup = options.__testHookLookup || lookupAddresses;
    const hostname = clientRequestArgsToHostName(options);
    const addresses = await lookup(hostname);
    const dnsLookupAt = monotonicTime();
    const sockets = new Set();
    let firstError;
    let errorCount = 0;
    const handleError = (socket, err) => {
        if (!sockets.delete(socket))
            return;
        ++errorCount;
        firstError ??= err;
        if (errorCount === addresses.length)
            oncreate?.(firstError);
    };
    const connected = new ManualPromise();
    for (const { address } of addresses) {
        const socket = useTLS ?
            tls.connect({
                ...options,
                port: options.port,
                host: address,
                servername: hostname
            }) :
            net.createConnection({
                ...options,
                port: options.port,
                host: address
            });
        socket[kDNSLookupAt] = dnsLookupAt;
        // Each socket may fire only one of 'connect', 'timeout' or 'error' events.
        // None of these events are fired after socket.destroy() is called.
        socket.on('connect', () => {
            socket[kTCPConnectionAt] = monotonicTime();
            connected.resolve();
            oncreate?.(null, socket);
            // TODO: Cache the result?
            // Close other outstanding sockets.
            sockets.delete(socket);
            for (const s of sockets)
                s.destroy();
            sockets.clear();
        });
        socket.on('timeout', () => {
            // Timeout is not an error, so we have to manually close the socket.
            socket.destroy();
            handleError(socket, new Error('Connection timeout'));
        });
        socket.on('error', e => handleError(socket, e));
        sockets.add(socket);
        await Promise.race([
            connected,
            new Promise(f => setTimeout(f, connectionAttemptDelayMs))
        ]);
        if (connected.isDone())
            break;
    }
}
async function lookupAddresses(hostname) {
    // Use separate family lookups to avoid AI_ADDRCONFIG filtering. When family: 0 is used,
    // Node.js passes AI_ADDRCONFIG to getaddrinfo(), which on macOS can filter out addresses
    // for a family that has no non-loopback interface — e.g. returning only 127.0.0.1 for
    // "localhost" when IPv6 is not available on non-loopback interfaces, even though ::1 is
    // present in /etc/hosts. Separate family: 4 and family: 6 lookups do not pass AI_ADDRCONFIG.
    const [v4Result, v6Result] = await Promise.allSettled([
        dns.promises.lookup(hostname, { all: true, family: 4 }),
        dns.promises.lookup(hostname, { all: true, family: 6 }),
    ]);
    const v4Addresses = v4Result.status === 'fulfilled' ? v4Result.value : [];
    const v6Addresses = v6Result.status === 'fulfilled' ? v6Result.value : [];
    if (!v4Addresses.length && !v6Addresses.length) {
        if (v4Result.status === 'rejected')
            throw v4Result.reason;
        throw v6Result.reason;
    }
    const result = [];
    // Alternate IPv6 and IPv4 addresses per RFC 8305 (prefer IPv6 first).
    for (let i = 0; i < Math.max(v4Addresses.length, v6Addresses.length); i++) {
        if (v6Addresses[i])
            result.push(v6Addresses[i]);
        if (v4Addresses[i])
            result.push(v4Addresses[i]);
    }
    return result;
}
function clientRequestArgsToHostName(options) {
    if (options.hostname)
        return options.hostname;
    if (options.host)
        return options.host;
    throw new Error('Either options.hostname or options.host must be provided');
}
export function timingForSocket(socket) {
    return {
        dnsLookupAt: socket[kDNSLookupAt],
        tcpConnectionAt: socket[kTCPConnectionAt],
    };
}
//# sourceMappingURL=happyEyeballs.js.map