import HttpServerPort, { HttpRequest, HttpResponse } from "../../ports/HttpServerPort.js";
import { MAX_BODY_BYTES, PayloadTooLargeError, noopLogger } from "./httpAdapterUtils.js";
import { createServer } from 'node:http';
import WarpError from "../../domain/errors/WarpError.js";
/**
 * Reads the request body from a Node.js IncomingMessage stream, enforcing a size limit.
 */
async function readBody(req) {
    const chunks = [];
    let totalBytes = 0;
    for await (const chunk of req) {
        totalBytes += chunk.length;
        if (totalBytes > MAX_BODY_BYTES) {
            req.destroy();
            throw new PayloadTooLargeError(totalBytes);
        }
        chunks.push(chunk);
    }
    return Buffer.concat(chunks);
}
/** Converts Node's incoming header shape into the port header contract. */
function normalizeNodeHeaders(headers) {
    const normalized = {};
    for (const [key, value] of Object.entries(headers)) {
        if (typeof value === 'string') {
            normalized[key] = value;
        }
        else if (Array.isArray(value)) {
            normalized[key] = value.join(', ');
        }
    }
    return normalized;
}
/**
 * Builds an HttpRequest from a Node.js IncomingMessage and body buffer.
 */
function buildHttpRequest(req, body) {
    return new HttpRequest({
        method: req.method ?? '',
        url: req.url ?? '',
        headers: normalizeNodeHeaders(req.headers),
        body: body.length > 0 ? body : undefined,
    });
}
/**
 * Handles a dispatch error, sending the appropriate response.
 */
function handleDispatchError(err, res, logger) {
    if (err instanceof PayloadTooLargeError) {
        res.writeHead(413, { 'Content-Type': 'text/plain' });
        res.end('Payload Too Large');
        return;
    }
    logger.error('[NodeHttpAdapter] dispatch error:', err);
    if (!res.headersSent) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
    }
    res.end('Internal Server Error');
}
/**
 * Sends the handler response back through the Node.js ServerResponse.
 */
function sendResponse(response, res) {
    const validated = HttpResponse.from(response);
    res.writeHead(validated.status ?? 200, validated.headers ?? {});
    res.end(validated.body ?? undefined);
}
/**
 * Collects the request body and dispatches to the handler, returning
 * a 500 response if the handler throws.
 */
async function dispatch(req, res, { handler, logger }) {
    try {
        const body = await readBody(req);
        const request = buildHttpRequest(req, body);
        const response = await handler(request);
        sendResponse(response, res);
    }
    catch (err) {
        handleDispatchError(err, res, logger);
    }
}
/**
 * Node.js HTTP adapter implementing HttpServerPort.
 *
 * This is the only file that imports node:http for server creation.
 */
export default class NodeHttpAdapter extends HttpServerPort {
    _logger;
    constructor(options) {
        super();
        const { logger } = options ?? {};
        this._logger = logger ?? noopLogger;
    }
    createServer(requestHandler) {
        const server = buildNodeServer(requestHandler, this._logger);
        return createServerHandle(server);
    }
}
/**
 * Creates a Node.js HTTP server that dispatches requests through the handler.
 */
function buildNodeServer(requestHandler, logger) {
    return createServer((req, res) => {
        dispatch(req, res, { handler: requestHandler, logger }).catch((err) => {
            logger.error('[NodeHttpAdapter] unhandled dispatch error:', err);
        });
    });
}
/**
 * Wraps a Node.js HTTP server in the HttpServerHandle interface.
 */
function createServerHandle(server) {
    return {
        listen(port, host, callback) {
            const cb = typeof host === 'function' ? host : callback;
            const bindHost = typeof host === 'string' ? host : undefined;
            startListening({ server, port, bindHost, cb });
        },
        close(callback) {
            server.close(callback);
        },
        address() {
            return server.address();
        },
    };
}
/**
 * Starts listening on the server, branching on whether a host is specified.
 */
function startListening({ server, port, bindHost, cb }) {
    const onError = (err) => {
        if (cb) {
            cb(err instanceof Error ? err : new WarpError(String(err), 'E_HTTP_LISTEN'));
        }
    };
    const onListening = () => {
        server.removeListener('error', onError);
        if (cb) {
            cb(null);
        }
    };
    server.once('error', onError);
    if (bindHost !== undefined) {
        server.listen(port, bindHost, onListening);
    }
    else {
        server.listen(port, onListening);
    }
}
