import { generateUuid } from '../../../common/utility.js';
import { RemoteSignedJson } from '../../../common/RemoteSignedJson.js';
import { Cloudflare } from '../../../config/cloudflare.js';
import { Env } from '../../../config/env.js';
import { Logger } from '../../../config/logger.js';
import { ErrorFactory } from '../../../exceptions/ZintrustError.js';
import * as net from '../../../node-singletons/net.js';
import * as tls from '../../../node-singletons/tls.js';
import { normalizeSigningCredentials } from '../../../proxy/SigningService.js';
const resolveSigningPrefix = (baseUrl) => {
    try {
        const parsed = new URL(baseUrl);
        const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
        if (path === '' || path === '/')
            return undefined;
        return path;
    }
    catch {
        return undefined;
    }
};
const normalizeRecipients = (to) => (Array.isArray(to) ? to : [to]);
const toBase64 = (value) => Buffer.from(value, 'utf8').toString('base64');
const isNodeRuntime = () => typeof process !== 'undefined' && typeof process.versions?.node === 'string';
const isWorkersRuntime = () => Cloudflare.getWorkersEnv() !== null;
const validateConfig = (config) => {
    if (!isNodeRuntime() && !isWorkersRuntime()) {
        throw ErrorFactory.createConfigError('SMTP driver requires Node.js or Workers runtime');
    }
    if (config.host.trim() === '') {
        throw ErrorFactory.createConfigError('SMTP: missing MAIL_HOST');
    }
    if (!Number.isFinite(config.port) || config.port <= 0) {
        throw ErrorFactory.createConfigError('SMTP: invalid MAIL_PORT');
    }
};
const resolveProxyBaseUrl = () => {
    const explicit = Env.SMTP_PROXY_URL.trim();
    if (explicit !== '')
        return explicit;
    const host = Env.SMTP_PROXY_HOST || '127.0.0.1';
    const port = Env.SMTP_PROXY_PORT;
    return `http://${host}:${port}`;
};
const buildProxySettings = () => {
    const baseUrl = resolveProxyBaseUrl();
    const keyId = Env.SMTP_PROXY_KEY_ID ?? '';
    const secret = Env.SMTP_PROXY_SECRET ?? '';
    const timeoutMs = Env.SMTP_PROXY_TIMEOUT_MS;
    return { baseUrl, keyId, secret, timeoutMs };
};
const buildSignedSettings = (settings) => {
    const creds = normalizeSigningCredentials({
        keyId: settings.keyId ?? '',
        secret: settings.secret ?? '',
    });
    return {
        baseUrl: settings.baseUrl,
        keyId: creds.keyId,
        secret: creds.secret,
        timeoutMs: settings.timeoutMs,
        signaturePathPrefixToStrip: resolveSigningPrefix(settings.baseUrl),
        missingUrlMessage: 'SMTP proxy URL is missing (SMTP_PROXY_URL)',
        missingCredentialsMessage: 'SMTP proxy signing credentials are missing (SMTP_PROXY_KEY_ID / SMTP_PROXY_SECRET)',
        messages: {
            unauthorized: 'SMTP proxy unauthorized',
            forbidden: 'SMTP proxy forbidden',
            rateLimited: 'SMTP proxy rate limited',
            rejected: 'SMTP proxy rejected request',
            error: 'SMTP proxy error',
            timedOut: 'SMTP proxy request timed out',
        },
    };
};
const ensureSignedSettings = (settings) => {
    const signedSettings = buildSignedSettings(settings);
    if (signedSettings.baseUrl.trim() === '') {
        throw ErrorFactory.createConfigError('SMTP proxy URL is missing (SMTP_PROXY_URL)');
    }
    if (signedSettings.keyId.trim() === '' || signedSettings.secret.trim() === '') {
        throw ErrorFactory.createConfigError('SMTP proxy signing credentials are missing (SMTP_PROXY_KEY_ID / SMTP_PROXY_SECRET)');
    }
    return signedSettings;
};
const shouldUseProxy = () => {
    if (Env.USE_SMTP_PROXY === true)
        return true;
    // In Cloudflare Workers, SMTP requires TCP sockets; if sockets are disabled,
    // we must use the HTTP SMTP proxy transport when a URL is configured.
    if (isWorkersRuntime() && Cloudflare.isCloudflareSocketsEnabled() === false) {
        return Env.SMTP_PROXY_URL.trim() !== '';
    }
    // Default: do not use proxy unless explicitly enabled.
    return false;
};
const serializeMessage = (message) => {
    const attachments = message.attachments?.map((attachment) => {
        const raw = Buffer.isBuffer(attachment.content)
            ? attachment.content
            : Buffer.from(attachment.content);
        return {
            filename: attachment.filename,
            contentBase64: raw.toString('base64'),
        };
    });
    return {
        to: message.to,
        from: message.from,
        subject: message.subject,
        text: message.text,
        html: message.html,
        attachments: attachments && attachments.length > 0 ? attachments : undefined,
    };
};
const sendViaProxy = async (message) => {
    const settings = buildProxySettings();
    const signedSettings = ensureSignedSettings(settings);
    return RemoteSignedJson.request(signedSettings, '/zin/smtp/send', {
        message: serializeMessage(message),
    });
};
const loadCloudflareSocketFactory = async () => {
    const mod = (await import('../../../sockets/CloudflareSocket.js'));
    const record = mod;
    const socketFactory = record.CloudflareSocket;
    if (!socketFactory || typeof socketFactory.create !== 'function') {
        throw ErrorFactory.createConnectionError('Cloudflare socket factory unavailable');
    }
    return socketFactory;
};
const createNodeSocket = async (config, implicitTls) => {
    validateConfig(config);
    return new Promise((resolve, reject) => {
        const onError = (err) => {
            reject(ErrorFactory.createConnectionError('SMTP connection failed', {
                host: config.host,
                port: config.port,
                secure: implicitTls,
                error: err,
            }));
        };
        const socket = implicitTls
            ? tls.connect({
                host: config.host,
                port: config.port,
                servername: config.host,
            })
            : net.connect({ host: config.host, port: config.port });
        if (typeof socket.setTimeout === 'function') {
            socket.setTimeout(10000);
        }
        socket.once('timeout', () => {
            socket.destroy();
            reject(ErrorFactory.createConnectionError('SMTP connection timed out', {
                host: config.host,
                port: config.port,
            }));
        });
        if (implicitTls) {
            socket.once('secureConnect', () => resolve(socket));
        }
        else {
            socket.once('connect', () => resolve(socket));
        }
        socket.once('error', onError);
    });
};
const createWorkersSocket = async (config, implicitTls) => {
    validateConfig(config);
    return new Promise((resolve, reject) => {
        let cleanup = () => undefined;
        const onError = (err) => {
            cleanup();
            reject(ErrorFactory.createConnectionError('SMTP connection failed', {
                host: config.host,
                port: config.port,
                secure: implicitTls,
                error: err,
            }));
        };
        const socketFactoryPromise = loadCloudflareSocketFactory();
        socketFactoryPromise
            .then((socketFactory) => {
            const socket = socketFactory.create(config.host, config.port, {
                tls: implicitTls,
            });
            if (!socket) {
                reject(ErrorFactory.createConnectionError('SMTP socket initialization failed'));
                return;
            }
            const onConnect = () => {
                cleanup(socket);
                resolve(socket);
            };
            cleanup = (target) => {
                if (!target)
                    return;
                if (typeof target.off === 'function') {
                    target.off('connect', onConnect);
                    target.off('error', onError);
                }
                else if (typeof target.removeListener === 'function') {
                    target.removeListener('connect', onConnect);
                    target.removeListener('error', onError);
                }
            };
            socket.once('connect', onConnect);
            socket.once('error', onError);
        })
            .catch((err) => {
            reject(ErrorFactory.createConnectionError('SMTP socket initialization failed', {
                error: err,
            }));
        });
    });
};
const createSocket = async (config, implicitTls) => {
    if (isWorkersRuntime())
        return createWorkersSocket(config, implicitTls);
    return createNodeSocket(config, implicitTls);
};
const upgradeToStartTls = async (socket, host) => {
    if (typeof socket.startTls === 'function') {
        await socket.startTls();
        return socket;
    }
    return new Promise((resolve, reject) => {
        const tlsSocket = tls.connect({ socket: socket, servername: host });
        tlsSocket.once('secureConnect', () => {
            resolve(tlsSocket);
        });
        tlsSocket.once('error', (err) => {
            reject(ErrorFactory.createConnectionError('SMTP STARTTLS upgrade failed', {
                host,
                error: err,
            }));
        });
    });
};
const createLineReader = (socket) => {
    let buffer = '';
    const waiters = [];
    const wake = () => {
        while (waiters.length > 0) {
            const w = waiters.shift();
            if (w)
                w();
        }
    };
    const onData = (data) => {
        const chunk = data instanceof Uint8Array ? data : Buffer.from(String(data));
        buffer += Buffer.from(chunk).toString('utf8');
        wake();
    };
    socket.on('data', onData);
    const tryReadLineFromBuffer = () => {
        const idx = buffer.indexOf('\n');
        if (idx < 0)
            return undefined;
        const line = buffer.slice(0, idx + 1);
        buffer = buffer.slice(idx + 1);
        return line.replace(/\r?\n$/, '');
    };
    const readLine = async () => {
        const immediate = tryReadLineFromBuffer();
        if (typeof immediate === 'string')
            return immediate;
        await new Promise((resolve, reject) => {
            let fired = false;
            const timer = globalThis.setTimeout(() => {
                fired = true;
                reject(ErrorFactory.createConnectionError('SMTP read timeout'));
            }, 30000);
            waiters.push(() => {
                if (fired)
                    return;
                clearTimeout(timer);
                resolve();
            });
        });
        return readLine();
    };
    const readMultiline = async (code, message) => {
        const line = await readLine();
        const nextCodeStr = line.slice(0, 3);
        const nextCode = Number.parseInt(nextCodeStr, 10);
        const nextMsg = line.length > 4 ? line.slice(4) : '';
        const nextMessage = `${message}\n${nextMsg}`.trim();
        if (Number.isFinite(nextCode) && nextCode === code && line[3] === ' ')
            return nextMessage;
        return readMultiline(code, nextMessage);
    };
    const readResponse = async () => {
        const first = await readLine();
        const codeStr = first.slice(0, 3);
        const code = Number.parseInt(codeStr, 10);
        if (!Number.isFinite(code)) {
            throw ErrorFactory.createConnectionError('SMTP: invalid response code', { first });
        }
        let message = first.length > 4 ? first.slice(4) : '';
        // Multiline response: "250-..." then "250 ..."
        if (first[3] === '-') {
            message = await readMultiline(code, message);
        }
        return { code, message };
    };
    const close = () => {
        if (typeof socket.off === 'function') {
            socket.off('data', onData);
        }
    };
    return { readResponse, close };
};
const writeRaw = async (socket, data) => {
    return new Promise((resolve, reject) => {
        let settled = false;
        const finish = (err) => {
            if (settled)
                return;
            settled = true;
            if (err)
                reject(err);
            else
                resolve();
        };
        try {
            const writer = socket;
            writer.write(data, (err) => finish(err));
            if (writer.write.length < 2) {
                finish();
            }
        }
        catch (err) {
            finish(err);
        }
    });
};
const writeLine = async (socket, line) => {
    return writeRaw(socket, `${line}\r\n`);
};
const assertCode = (res, expected, context) => {
    const allowed = Array.isArray(expected) ? expected : [expected];
    if (!allowed.includes(res.code)) {
        throw ErrorFactory.createConnectionError(`SMTP: unexpected response for ${context}`, {
            expected: allowed,
            got: res.code,
            message: res.message,
        });
    }
};
const hasCapability = (res, capability) => {
    const cap = capability.trim().toUpperCase();
    if (cap === '')
        return false;
    return res.message
        .split('\n')
        .some((line) => line.trim().toUpperCase().startsWith(cap) || line.trim().toUpperCase().includes(cap));
};
const doEhlo = async (socket, reader) => {
    await writeLine(socket, 'EHLO zintrust');
    const res = await reader.readResponse();
    assertCode(res, 250, 'EHLO');
    return res;
};
const doAuthLoginIfNeeded = async (socket, reader, config) => {
    const username = config.username ?? '';
    const password = config.password ?? '';
    const wantsAuth = username.trim() !== '' || password.trim() !== '';
    if (!wantsAuth)
        return;
    if (username.trim() === '' || password.trim() === '') {
        throw ErrorFactory.createConfigError('SMTP: both MAIL_USERNAME and MAIL_PASSWORD are required');
    }
    // Try AUTH PLAIN first (more widely supported)
    try {
        // AUTH PLAIN format: base64("\0username\0password")
        const authPlainString = `\0${username}\0${password}`;
        const authPlainEncoded = toBase64(authPlainString);
        await writeLine(socket, `AUTH PLAIN ${authPlainEncoded}`);
        const authPlain = await reader.readResponse();
        assertCode(authPlain, 235, 'AUTH PLAIN');
        return; // Success!
    }
    catch (plainError) {
        // AUTH PLAIN failed, try AUTH LOGIN as fallback
        try {
            await writeLine(socket, 'AUTH LOGIN');
            const auth1 = await reader.readResponse();
            assertCode(auth1, 334, 'AUTH LOGIN');
            await writeLine(socket, toBase64(username));
            const auth2 = await reader.readResponse();
            assertCode(auth2, 334, 'AUTH username');
            await writeLine(socket, toBase64(password));
            const auth3 = await reader.readResponse();
            assertCode(auth3, 235, 'AUTH password');
        }
        catch (loginError) {
            // Both methods failed, throw the original error
            throw ErrorFactory.createConnectionError('SMTP authentication failed', {
                error: plainError,
                loginError,
            });
        }
    }
};
const doMailFrom = async (socket, reader, fromEmail) => {
    await writeLine(socket, `MAIL FROM:<${fromEmail}>`);
    const mailFrom = await reader.readResponse();
    assertCode(mailFrom, [250, 251], 'MAIL FROM');
};
const doRcptToAll = async (socket, reader, recipients) => {
    if (recipients.length === 0) {
        throw ErrorFactory.createValidationError('SMTP: missing recipients');
    }
    await recipients.reduce(async (prev, rcpt) => {
        await prev;
        await writeLine(socket, `RCPT TO:<${rcpt}>`);
        const rcptRes = await reader.readResponse();
        assertCode(rcptRes, [250, 251], 'RCPT TO');
    }, Promise.resolve());
};
const doData = async (socket, reader, message) => {
    await writeLine(socket, 'DATA');
    const dataRes = await reader.readResponse();
    assertCode(dataRes, 354, 'DATA');
    const raw = buildRfc2822Message(message);
    const stuffed = dotStuff(raw);
    await writeRaw(socket, `${stuffed}\r\n.\r\n`);
    const queued = await reader.readResponse();
    assertCode(queued, 250, 'message body');
};
const doQuit = async (socket, reader) => {
    await writeLine(socket, 'QUIT');
    const quit = await reader.readResponse();
    assertCode(quit, [221, 250], 'QUIT');
};
const buildRfc2822Message = (msg) => {
    const toList = normalizeRecipients(msg.to);
    const fromNameRaw = msg.from.name;
    const fromName = typeof fromNameRaw === 'string' ? fromNameRaw.trim() : '';
    const fromHeader = fromName === '' ? msg.from.email : `${fromName} <${msg.from.email}>`;
    const toHeader = toList.join(', ');
    const subject = msg.subject;
    const headers = [
        `From: ${fromHeader}`,
        `To: ${toHeader}`,
        `Subject: ${subject}`,
        'MIME-Version: 1.0',
    ];
    const attachParts = (attachments, innerBody) => {
        const mixedBoundary = `mixed_${generateUuid().replaceAll('-', '')}`;
        const lines = [];
        lines.push(`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`, '', `--${mixedBoundary}`, innerBody);
        // attachments
        for (const a of attachments) {
            const b64 = a.content.toString('base64');
            lines.push(`--${mixedBoundary}`, `Content-Type: application/octet-stream; name="${a.filename}"`, 'Content-Transfer-Encoding: base64', `Content-Disposition: attachment; filename="${a.filename}"`, '');
            // break base64 into 76 char lines per RFC
            for (let i = 0; i < b64.length; i += 76) {
                lines.push(b64.slice(i, i + 76));
            }
        }
        lines.push(`--${mixedBoundary}--`, '');
        return lines.join('\r\n');
    };
    if (typeof msg.html === 'string' && msg.html !== '') {
        const boundary = `zintrust_${generateUuid().replaceAll('-', '')}`;
        headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
        const parts = [
            `--${boundary}`,
            'Content-Type: text/plain; charset=utf-8',
            'Content-Transfer-Encoding: 7bit',
            '',
            msg.text,
            `--${boundary}`,
            'Content-Type: text/html; charset=utf-8',
            'Content-Transfer-Encoding: 7bit',
            '',
            msg.html,
            `--${boundary}--`,
            '',
        ];
        const inner = `${parts.join('\r\n')}`;
        if (msg.attachments && msg.attachments.length > 0) {
            // wrap in multipart/mixed
            const mixed = attachParts(msg.attachments, inner);
            return `${headers.join('\r\n')}\r\n\r\n${mixed}`;
        }
        return `${headers.join('\r\n')}\r\n\r\n${inner}`;
    }
    // plain text
    if (msg.attachments && msg.attachments.length > 0) {
        const inner = ['Content-Type: text/plain; charset=utf-8', '', msg.text, ''].join('\r\n');
        const mixed = attachParts(msg.attachments, inner);
        return `${headers.join('\r\n')}\r\n\r\n${mixed}`;
    }
    headers.push('Content-Type: text/plain; charset=utf-8');
    return `${headers.join('\r\n')}\r\n\r\n${msg.text}\r\n`;
};
const dotStuff = (data) => data
    .replaceAll(/\r?\n/g, '\r\n')
    .split('\r\n')
    .map((line) => (line.startsWith('.') ? `.${line}` : line))
    .join('\r\n');
export const SmtpDriver = Object.freeze({
    /**
     * NOTE: This is a minimal SMTP implementation intended for Node.js runtimes.
     * - `secure=true` means TLS from the start (SMTPS).
     * - `secure='starttls'` performs STARTTLS after EHLO.
     */
    async send(config, message) {
        if (shouldUseProxy()) {
            const proxyUrl = Env.SMTP_PROXY_URL;
            Logger.debug('[SmtpDriver] Using SmtpProxy', { proxyUrl });
            const result = await sendViaProxy(message);
            return { ok: Boolean(result.ok), provider: 'smtp', messageId: result.messageId };
        }
        let mode = 'none';
        if (config.secure === true)
            mode = 'tls';
        if (config.secure === 'starttls')
            mode = 'starttls';
        let socket = await createSocket(config, mode === 'tls');
        let reader = createLineReader(socket);
        try {
            const greeting = await reader.readResponse();
            assertCode(greeting, 220, 'greeting');
            const ehlo = await doEhlo(socket, reader);
            if (mode === 'starttls') {
                const supportsStartTls = hasCapability(ehlo, 'STARTTLS');
                if (!supportsStartTls) {
                    throw ErrorFactory.createConnectionError('SMTP server does not support STARTTLS');
                }
                await writeLine(socket, 'STARTTLS');
                const startTls = await reader.readResponse();
                assertCode(startTls, 220, 'STARTTLS');
                // Swap reader/socket to the upgraded TLS stream
                reader.close();
                socket = await upgradeToStartTls(socket, config.host);
                reader = createLineReader(socket);
                // RFC: EHLO again after STARTTLS
                await doEhlo(socket, reader);
            }
            await doAuthLoginIfNeeded(socket, reader, config);
            await doMailFrom(socket, reader, message.from.email);
            await doRcptToAll(socket, reader, normalizeRecipients(message.to));
            await doData(socket, reader, message);
            await doQuit(socket, reader);
            return { ok: true, provider: 'smtp' };
        }
        catch (err) {
            throw ErrorFactory.createConnectionError('SMTP send failed', { error: err });
        }
        finally {
            reader.close();
            socket.end();
        }
    },
});
export default SmtpDriver;
