import { errorMonitor } from 'node:events';
import { types } from 'node:util';
import deferToConnect from './defer-to-connect.js';
const getInitialConnectionTimings = (socket) => Reflect.get(socket, '__initial_connection_timings__');
const setInitialConnectionTimings = (socket, timings) => {
    Reflect.set(socket, '__initial_connection_timings__', timings);
};
const timer = (request) => {
    if (request.timings) {
        return request.timings;
    }
    const timings = {
        start: Date.now(),
        socket: undefined,
        lookup: undefined,
        connect: undefined,
        secureConnect: undefined,
        upload: undefined,
        response: undefined,
        end: undefined,
        error: undefined,
        abort: undefined,
        phases: {
            wait: undefined,
            dns: undefined,
            tcp: undefined,
            tls: undefined,
            request: undefined,
            firstByte: undefined,
            download: undefined,
            total: undefined,
        },
    };
    request.timings = timings;
    const handleError = (origin) => {
        origin.once(errorMonitor, () => {
            timings.error = Date.now();
            timings.phases.total = timings.error - timings.start;
        });
    };
    handleError(request);
    const onAbort = () => {
        timings.abort = Date.now();
        timings.phases.total = timings.abort - timings.start;
    };
    request.prependOnceListener('abort', onAbort);
    const onSocket = (socket) => {
        timings.socket = Date.now();
        timings.phases.wait = timings.socket - timings.start;
        if (types.isProxy(socket)) {
            // HTTP/2: The socket is a proxy, so connection events won't fire.
            // We can't measure connection timings, so leave them undefined.
            // This prevents NaN in phases.request calculation.
            return;
        }
        // Check if socket is already connected (reused from connection pool)
        const socketAlreadyConnected = socket.writable && !socket.connecting;
        if (socketAlreadyConnected) {
            // Socket reuse detected: the socket was already connected from a previous request.
            // For reused sockets, set all connection timestamps to socket time since no new
            // connection was made for THIS request. But preserve phase durations from the
            // original connection so they're not lost.
            timings.lookup = timings.socket;
            timings.connect = timings.socket;
            const initialConnectionTimings = getInitialConnectionTimings(socket);
            if (initialConnectionTimings) {
                // Restore the phase timings from the initial connection
                timings.phases.dns = initialConnectionTimings.dnsPhase;
                timings.phases.tcp = initialConnectionTimings.tcpPhase;
                timings.phases.tls = initialConnectionTimings.tlsPhase;
                // Set secureConnect timestamp if there was TLS
                if (timings.phases.tls !== undefined) {
                    timings.secureConnect = timings.socket;
                }
            }
            else {
                // Socket reused but no initial timings stored (e.g., from external code)
                // Set phases to 0
                timings.phases.dns = 0;
                timings.phases.tcp = 0;
            }
            return;
        }
        const lookupListener = () => {
            timings.lookup = Date.now();
            timings.phases.dns = timings.lookup - timings.socket;
        };
        socket.prependOnceListener('lookup', lookupListener);
        deferToConnect(socket, {
            connect() {
                timings.connect = Date.now();
                if (timings.lookup === undefined) {
                    // No DNS lookup occurred (e.g., connecting to an IP address directly)
                    // Set lookup to socket time (no time elapsed for DNS)
                    socket.removeListener('lookup', lookupListener);
                    timings.lookup = timings.socket;
                    timings.phases.dns = 0;
                }
                timings.phases.tcp = timings.connect - timings.lookup;
                // If lookup and connect happen at the EXACT same time (tcp = 0),
                // DNS was served from cache and the dns value is just event loop overhead.
                // Set dns to 0 to indicate no actual DNS resolution occurred.
                // Fixes https://github.com/szmarczak/http-timer/issues/35
                if (timings.phases.tcp === 0 && timings.phases.dns && timings.phases.dns > 0) {
                    timings.phases.dns = 0;
                }
                // Store connection phase timings on socket for potential reuse
                if (!getInitialConnectionTimings(socket)) {
                    setInitialConnectionTimings(socket, {
                        dnsPhase: timings.phases.dns,
                        // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript can't prove this is defined due to callback structure
                        tcpPhase: timings.phases.tcp,
                    });
                }
            },
            secureConnect() {
                timings.secureConnect = Date.now();
                timings.phases.tls = timings.secureConnect - timings.connect;
                // Update stored timings with TLS phase timing
                const initialConnectionTimings = getInitialConnectionTimings(socket);
                if (initialConnectionTimings) {
                    initialConnectionTimings.tlsPhase = timings.phases.tls;
                }
            },
        });
    };
    if (request.socket) {
        onSocket(request.socket);
    }
    else {
        request.prependOnceListener('socket', onSocket);
    }
    const onUpload = () => {
        timings.upload = Date.now();
        // Calculate request phase if we have connection timings
        const secureOrConnect = timings.secureConnect ?? timings.connect;
        if (secureOrConnect !== undefined) {
            timings.phases.request = timings.upload - secureOrConnect;
        }
        // If both are undefined (HTTP/2), phases.request stays undefined (not NaN)
    };
    if (request.writableFinished) {
        onUpload();
    }
    else {
        request.prependOnceListener('finish', onUpload);
    }
    request.prependOnceListener('response', (response) => {
        timings.response = Date.now();
        timings.phases.firstByte = timings.response - timings.upload;
        response.timings = timings;
        handleError(response);
        response.prependOnceListener('end', () => {
            request.off('abort', onAbort);
            response.off('aborted', onAbort);
            if (timings.phases.total !== undefined) {
                // Aborted or errored
                return;
            }
            timings.end = Date.now();
            timings.phases.download = timings.end - timings.response;
            timings.phases.total = timings.end - timings.start;
        });
        response.prependOnceListener('aborted', onAbort);
    });
    return timings;
};
export default timer;
