'use strict';

const httpProxyClient = require('nodemailer/lib/smtp-connection/http-proxy-client');
const { SocksClient } = require('socks');
const util = require('util');
const httpProxyClientAsync = util.promisify(httpProxyClient);
const dns = require('dns').promises;
const net = require('net');

// Redacts the password from a parsed URL object before it is logged,
// preventing credentials from appearing in log output.
const hidePassword = proxyUrl => {
    if (proxyUrl.password) {
        proxyUrl.password = '(hidden)';
    }
};

// Attaches a benign 'error' listener as soon as the proxied socket exists, so an early
// socket error (before ImapFlow installs its own handlers) cannot surface as an unhandled
// 'error' event and crash the process. The handler is stored on the socket so the caller
// can remove it once it takes ownership of the socket.
const attachEarlyErrorHandler = (logger, socket) => {
    if (!socket || typeof socket.on !== 'function') {
        return;
    }
    socket._earlyErrorHandler = err => {
        logger.error({ msg: 'Proxy socket error before connection setup', err });
    };
    socket.on('error', socket._earlyErrorHandler);
};

// Removes the handler installed by attachEarlyErrorHandler once the caller takes ownership
// of the socket. Keeps the internal `_earlyErrorHandler` contract inside this module.
const detachEarlyErrorHandler = socket => {
    if (socket && socket._earlyErrorHandler) {
        socket.removeListener('error', socket._earlyErrorHandler);
        socket._earlyErrorHandler = null;
    }
};

const proxyConnection = async (logger, connectionUrl, host, port) => {
    let proxyUrl = new URL(connectionUrl);

    let protocol = proxyUrl.protocol.replace(/:$/, '').toLowerCase();

    // Pre-resolve the IMAP server hostname to an IP address before passing it to the proxy.
    // Some proxy implementations (especially SOCKS4) do not support hostname resolution,
    // so we resolve DNS on the client side to ensure compatibility.
    if (!net.isIP(host)) {
        let resolveResult = await dns.resolve(host);
        if (resolveResult && resolveResult.length) {
            host = resolveResult[0];
        }
    }

    switch (protocol) {
        // Connect using a HTTP CONNECT method
        case 'http':
        case 'https': {
            try {
                let socket = await httpProxyClientAsync(proxyUrl.href, port, host);
                if (socket) {
                    hidePassword(proxyUrl);
                    logger.info({
                        msg: 'Established a socket via HTTP proxy',
                        proxyUrl: proxyUrl.href,
                        port,
                        host
                    });
                    attachEarlyErrorHandler(logger, socket);
                }
                return socket;
            } catch (err) {
                hidePassword(proxyUrl);
                logger.error({
                    msg: 'Failed to establish a socket via HTTP proxy',
                    proxyUrl: proxyUrl.href,
                    port,
                    host,
                    err
                });
                throw err;
            }
        }

        // SOCKS proxy
        case 'socks':
        case 'socks5':
        case 'socks4':
        case 'socks4a': {
            let proxyType = Number(protocol.replace(/\D/g, '')) || 5;

            // targetHost here is the SOCKS proxy server's hostname (not the final IMAP destination).
            // The SOCKS library needs a resolved IP for the proxy host it connects to.
            // The final IMAP destination (host/port) is passed separately as 'destination'.
            let targetHost = proxyUrl.hostname;
            if (!net.isIP(targetHost)) {
                let resolveResult = await dns.resolve(targetHost);
                if (resolveResult && resolveResult.length) {
                    targetHost = resolveResult[0];
                }
            }

            let connectionOpts = {
                proxy: {
                    host: targetHost,
                    port: Number(proxyUrl.port) || 1080,
                    type: proxyType
                },
                destination: {
                    host,
                    port
                },
                command: 'connect',
                set_tcp_nodelay: true
            };

            if (proxyUrl.username || proxyUrl.password) {
                connectionOpts.proxy.userId = proxyUrl.username;
                connectionOpts.proxy.password = proxyUrl.password;
            }

            try {
                const info = await SocksClient.createConnection(connectionOpts);
                if (info && info.socket) {
                    hidePassword(proxyUrl);
                    logger.info({
                        msg: 'Established a socket via SOCKS proxy',
                        proxyUrl: proxyUrl.href,
                        port,
                        host
                    });
                    attachEarlyErrorHandler(logger, info.socket);
                }
                return info.socket;
            } catch (err) {
                hidePassword(proxyUrl);
                logger.error({
                    msg: 'Failed to establish a socket via SOCKS proxy',
                    proxyUrl: proxyUrl.href,
                    port,
                    host,
                    err
                });
                throw err;
            }
        }
    }
};

module.exports = { proxyConnection, detachEarlyErrorHandler };
