export const DEFAULT_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
export const DEFAULT_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
const DEFAULT_ERROR_MESSAGE = {
    PAYLOAD_TOO_LARGE: "PayloadTooLarge",
    REQUEST_BODY_TIMEOUT: "RequestBodyTimeout",
    CONNECTION_CLOSED: "RequestBodyConnectionClosed",
};
const DEFAULT_ERROR_STATUS_CODE = {
    PAYLOAD_TOO_LARGE: 413,
    REQUEST_BODY_TIMEOUT: 408,
    CONNECTION_CLOSED: 400,
};
const DEFAULT_RESPONSE_MESSAGE = {
    PAYLOAD_TOO_LARGE: "Payload too large",
    REQUEST_BODY_TIMEOUT: "Request body timeout",
    CONNECTION_CLOSED: "Connection closed",
};
export class RequestBodyLimitError extends Error {
    code;
    statusCode;
    constructor(init) {
        super(init.message ?? DEFAULT_ERROR_MESSAGE[init.code]);
        this.name = "RequestBodyLimitError";
        this.code = init.code;
        this.statusCode = DEFAULT_ERROR_STATUS_CODE[init.code];
    }
}
export function isRequestBodyLimitError(error, code) {
    if (!(error instanceof RequestBodyLimitError)) {
        return false;
    }
    if (!code) {
        return true;
    }
    return error.code === code;
}
export function requestBodyErrorToText(code) {
    return DEFAULT_RESPONSE_MESSAGE[code];
}
function parseContentLengthHeader(req) {
    const header = req.headers["content-length"];
    const raw = Array.isArray(header) ? header[0] : header;
    if (typeof raw !== "string") {
        return null;
    }
    const parsed = Number.parseInt(raw, 10);
    if (!Number.isFinite(parsed) || parsed < 0) {
        return null;
    }
    return parsed;
}
export async function readRequestBodyWithLimit(req, options) {
    const maxBytes = Number.isFinite(options.maxBytes)
        ? Math.max(1, Math.floor(options.maxBytes))
        : 1;
    const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
        ? Math.max(1, Math.floor(options.timeoutMs))
        : DEFAULT_WEBHOOK_BODY_TIMEOUT_MS;
    const encoding = options.encoding ?? "utf-8";
    const declaredLength = parseContentLengthHeader(req);
    if (declaredLength !== null && declaredLength > maxBytes) {
        const error = new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" });
        if (!req.destroyed) {
            // Limit violations are expected user input; destroying with an Error causes
            // an async 'error' event which can crash the process if no listener remains.
            req.destroy();
        }
        throw error;
    }
    return await new Promise((resolve, reject) => {
        let done = false;
        let ended = false;
        let totalBytes = 0;
        const chunks = [];
        const cleanup = () => {
            req.removeListener("data", onData);
            req.removeListener("end", onEnd);
            req.removeListener("error", onError);
            req.removeListener("close", onClose);
            clearTimeout(timer);
        };
        const finish = (cb) => {
            if (done) {
                return;
            }
            done = true;
            cleanup();
            cb();
        };
        const fail = (error) => {
            finish(() => reject(error));
        };
        const timer = setTimeout(() => {
            const error = new RequestBodyLimitError({ code: "REQUEST_BODY_TIMEOUT" });
            if (!req.destroyed) {
                req.destroy();
            }
            fail(error);
        }, timeoutMs);
        const onData = (chunk) => {
            if (done) {
                return;
            }
            const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
            totalBytes += buffer.length;
            if (totalBytes > maxBytes) {
                const error = new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" });
                if (!req.destroyed) {
                    req.destroy();
                }
                fail(error);
                return;
            }
            chunks.push(buffer);
        };
        const onEnd = () => {
            ended = true;
            finish(() => resolve(Buffer.concat(chunks).toString(encoding)));
        };
        const onError = (error) => {
            if (done) {
                return;
            }
            fail(error);
        };
        const onClose = () => {
            if (done || ended) {
                return;
            }
            fail(new RequestBodyLimitError({ code: "CONNECTION_CLOSED" }));
        };
        req.on("data", onData);
        req.on("end", onEnd);
        req.on("error", onError);
        req.on("close", onClose);
    });
}
export async function readJsonBodyWithLimit(req, options) {
    try {
        const raw = await readRequestBodyWithLimit(req, options);
        const trimmed = raw.trim();
        if (!trimmed) {
            if (options.emptyObjectOnEmpty === false) {
                return { ok: false, code: "INVALID_JSON", error: "empty payload" };
            }
            return { ok: true, value: {} };
        }
        try {
            return { ok: true, value: JSON.parse(trimmed) };
        }
        catch (error) {
            return {
                ok: false,
                code: "INVALID_JSON",
                error: error instanceof Error ? error.message : String(error),
            };
        }
    }
    catch (error) {
        if (isRequestBodyLimitError(error)) {
            return { ok: false, code: error.code, error: requestBodyErrorToText(error.code) };
        }
        return {
            ok: false,
            code: "INVALID_JSON",
            error: error instanceof Error ? error.message : String(error),
        };
    }
}
export function installRequestBodyLimitGuard(req, res, options) {
    const maxBytes = Number.isFinite(options.maxBytes)
        ? Math.max(1, Math.floor(options.maxBytes))
        : 1;
    const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs)
        ? Math.max(1, Math.floor(options.timeoutMs))
        : DEFAULT_WEBHOOK_BODY_TIMEOUT_MS;
    const responseFormat = options.responseFormat ?? "json";
    const customText = options.responseText ?? {};
    let tripped = false;
    let reason = null;
    let done = false;
    let ended = false;
    let totalBytes = 0;
    const cleanup = () => {
        req.removeListener("data", onData);
        req.removeListener("end", onEnd);
        req.removeListener("close", onClose);
        req.removeListener("error", onError);
        clearTimeout(timer);
    };
    const finish = () => {
        if (done) {
            return;
        }
        done = true;
        cleanup();
    };
    const respond = (error) => {
        const text = customText[error.code] ?? requestBodyErrorToText(error.code);
        if (!res.headersSent) {
            res.statusCode = error.statusCode;
            if (responseFormat === "text") {
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
                res.end(text);
            }
            else {
                res.setHeader("Content-Type", "application/json; charset=utf-8");
                res.end(JSON.stringify({ error: text }));
            }
        }
    };
    const trip = (error) => {
        if (tripped) {
            return;
        }
        tripped = true;
        reason = error.code;
        finish();
        respond(error);
        if (!req.destroyed) {
            // Limit violations are expected user input; destroying with an Error causes
            // an async 'error' event which can crash the process if no listener remains.
            req.destroy();
        }
    };
    const onData = (chunk) => {
        if (done) {
            return;
        }
        const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
        totalBytes += buffer.length;
        if (totalBytes > maxBytes) {
            trip(new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" }));
        }
    };
    const onEnd = () => {
        ended = true;
        finish();
    };
    const onClose = () => {
        if (done || ended) {
            return;
        }
        finish();
    };
    const onError = () => {
        finish();
    };
    const timer = setTimeout(() => {
        trip(new RequestBodyLimitError({ code: "REQUEST_BODY_TIMEOUT" }));
    }, timeoutMs);
    req.on("data", onData);
    req.on("end", onEnd);
    req.on("close", onClose);
    req.on("error", onError);
    const declaredLength = parseContentLengthHeader(req);
    if (declaredLength !== null && declaredLength > maxBytes) {
        trip(new RequestBodyLimitError({ code: "PAYLOAD_TOO_LARGE" }));
    }
    return {
        dispose: finish,
        isTripped: () => tripped,
        code: () => reason,
    };
}
