/**
 * Content Type Detection & Body Parsing Middleware
 * Detects content-type and parses non-JSON request bodies
 */
import { Env } from '../../config/env.js';
import { Logger } from '../../config/logger.js';
import { MultipartParser } from '../parsers/MultipartParser.js';
/**
 * Get content-type from request headers
 */
const getContentType = (req) => {
    const contentType = req.getHeader('content-type');
    if (typeof contentType === 'string')
        return contentType.split(';')[0].toLowerCase().trim();
    if (Array.isArray(contentType) && typeof contentType[0] === 'string') {
        return contentType[0].split(';')[0].toLowerCase().trim();
    }
    return '';
};
const shouldReadRequestBody = (req) => {
    const method = (req.getMethod?.() ?? 'GET').toUpperCase();
    if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS')
        return false;
    return true;
};
const toBuffer = (chunk) => {
    if (Buffer.isBuffer(chunk))
        return chunk;
    if (typeof chunk === 'string')
        return Buffer.from(chunk);
    if (chunk instanceof Uint8Array)
        return Buffer.from(chunk);
    if (chunk instanceof ArrayBuffer)
        return Buffer.from(new Uint8Array(chunk));
    return Buffer.from(String(chunk));
};
const validateSize = (bytes, maxBytes) => {
    if (bytes.length > maxBytes) {
        return { ok: false, statusCode: 413, message: 'Payload Too Large' };
    }
    return null;
};
const handleMockedStringBody = (body, maxBytes) => {
    const bytes = Buffer.from(body);
    const sizeError = validateSize(bytes, maxBytes);
    if (sizeError)
        return sizeError;
    return { ok: true, bytes, text: body };
};
const handleMockedBufferBody = (body, maxBytes) => {
    const sizeError = validateSize(body, maxBytes);
    if (sizeError)
        return sizeError;
    return { ok: true, bytes: body, text: body.toString('utf-8') };
};
const handleMockedObjectBody = (body, maxBytes) => {
    try {
        const text = JSON.stringify(body);
        const bytes = Buffer.from(text, 'utf-8');
        const sizeError = validateSize(bytes, maxBytes);
        if (sizeError)
            return sizeError;
        return { ok: true, bytes, text };
    }
    catch {
        return { ok: false, statusCode: 400, message: 'Invalid request body' };
    }
};
const handleMockedBody = (mockedBody, maxBytes) => {
    if (typeof mockedBody === 'string') {
        return handleMockedStringBody(mockedBody, maxBytes);
    }
    if (Buffer.isBuffer(mockedBody)) {
        return handleMockedBufferBody(mockedBody, maxBytes);
    }
    if (typeof mockedBody === 'object' && mockedBody !== null) {
        return handleMockedObjectBody(mockedBody, maxBytes);
    }
    return null;
};
const readStreamBody = async (raw, maxBytes) => {
    const chunks = [];
    let totalSize = 0;
    try {
        for await (const chunk of raw) {
            const buf = toBuffer(chunk);
            totalSize += buf.length;
            if (totalSize > maxBytes) {
                try {
                    raw.destroy?.();
                }
                catch {
                    // best-effort
                }
                return { ok: false, statusCode: 413, message: 'Payload Too Large' };
            }
            chunks.push(buf);
        }
    }
    catch {
        return { ok: false, statusCode: 400, message: 'Invalid request body' };
    }
    if (chunks.length === 0)
        return { ok: true, bytes: Buffer.from(''), text: '' };
    const bytes = Buffer.concat(chunks);
    return { ok: true, bytes, text: bytes.toString('utf-8') };
};
const readRawBody = async (req, maxBytes) => {
    const raw = req.getRaw();
    // Support tests/mocks that stuff a body directly on the raw request.
    const mockedBody = raw.body;
    const mockedResult = handleMockedBody(mockedBody, maxBytes);
    if (mockedResult) {
        return mockedResult;
    }
    // Read from actual stream
    return readStreamBody(raw, maxBytes);
};
const shouldStoreRawText = (contentType) => {
    if (contentType.includes('application/json'))
        return true;
    if (contentType.includes('application/x-www-form-urlencoded'))
        return true;
    if (contentType.startsWith('text/'))
        return true;
    if (contentType.includes('application/xml'))
        return true;
    return false;
};
const getTextFromRaw = (rawResult) => {
    if (typeof rawResult.text === 'string')
        return rawResult.text;
    return rawResult.bytes.toString('utf-8');
};
const convertExistingToRawResult = (existingBytes, existingText) => {
    if (Buffer.isBuffer(existingBytes)) {
        return { ok: true, bytes: existingBytes };
    }
    if (typeof existingText === 'string') {
        return { ok: true, bytes: Buffer.from(existingText, 'utf-8'), text: existingText };
    }
    // Should be unreachable (guarded by `hasExisting`), but keep a safe fallback.
    return { ok: true, bytes: Buffer.from('', 'utf-8'), text: '' };
};
const parseJsonBody = (text, contentType, res) => {
    try {
        return text === '' ? null : JSON.parse(text);
    }
    catch {
        Logger.debug('[Body Parser] Invalid JSON body', {
            contentType,
            byteLength: Buffer.byteLength(text),
            rawBodyPreview: text.slice(0, 256),
        });
        res.setStatus(400).json({ error: 'Invalid JSON body' });
        return null;
    }
};
const setRequestBody = (req, rawResult, contentType) => {
    const isUrlEncoded = contentType.includes('application/x-www-form-urlencoded');
    const isText = contentType.startsWith('text/') || contentType.includes('application/xml');
    const text = getTextFromRaw(rawResult);
    if (isUrlEncoded) {
        req.setBody(parseUrlEncodedBody(text));
    }
    else if (isText) {
        req.setBody(text);
    }
    else if (contentType !== '') {
        req.setBody(rawResult.bytes);
    }
};
const parseUrlEncodedBody = (text) => {
    const out = {};
    const params = new URLSearchParams(text);
    for (const [key, value] of params.entries()) {
        const existing = out[key];
        if (existing === undefined) {
            out[key] = value;
            continue;
        }
        if (Array.isArray(existing)) {
            existing.push(value);
            continue;
        }
        out[key] = [existing, value];
    }
    return out;
};
const processBodyParsing = async (req, res, contentType, maxBytes) => {
    const rawResult = await readRawBody(req, maxBytes);
    if (rawResult.ok === false) {
        res.setStatus(rawResult.statusCode).json({ error: rawResult.message });
        return false;
    }
    // Store raw body for downstream consumers
    req.context['rawBodyBytes'] = rawResult.bytes;
    if (shouldStoreRawText(contentType)) {
        req.context['rawBodyText'] = getTextFromRaw(rawResult);
    }
    else {
        req.context['rawBodyText'] = undefined;
    }
    // Parse and set body based on content type
    try {
        const isJson = contentType.includes('application/json');
        if (isJson) {
            const parsed = parseJsonBody(getTextFromRaw(rawResult), contentType, res);
            if (parsed === null && res.getStatus() === 400) {
                return false; // JSON parsing failed, response already sent
            }
            req.setBody(parsed);
        }
        else {
            setRequestBody(req, rawResult, contentType);
        }
        if (Env.getBool('ZIN_DEBUG_BODY_PARSING', false)) {
            Logger.debug('[Body Parser] Parsed request body', {
                contentType,
                byteLength: rawResult.bytes.length,
                parsedType: typeof req.getBody?.(),
            });
        }
    }
    catch (error) {
        Logger.error('[Body Parser] Unexpected error during body parsing', error);
    }
    return true;
};
/**
 * Body parsing middleware
 * Automatically detects content-type and parses non-JSON bodies
 */
export const bodyParsingMiddleware = async (req, res, next) => {
    const contentType = getContentType(req);
    // Early exit if body already set
    const existingBody = req.getBody?.();
    if (existingBody !== null && existingBody !== undefined) {
        await next();
        return;
    }
    // Early exit for multipart (handled by upload middleware)
    if (MultipartParser.isMultipart(contentType)) {
        await next();
        return;
    }
    // Early exit for methods that don't have bodies
    if (!shouldReadRequestBody(req)) {
        await next();
        return;
    }
    // Determine if we have existing raw body from adapter
    const existingBytes = req.context['rawBodyBytes'];
    const existingText = req.context['rawBodyText'];
    const hasExisting = Buffer.isBuffer(existingBytes) || typeof existingText === 'string';
    // Calculate size limit based on content type
    const isJson = contentType.includes('application/json');
    const maxJsonSize = Env.getInt('MAX_JSON_SIZE', 1024 * 1024);
    const maxBytes = isJson ? maxJsonSize : Env.MAX_BODY_SIZE;
    // Read or reuse raw body
    if (hasExisting) {
        const rawResult = convertExistingToRawResult(existingBytes, existingText);
        if (rawResult.ok) {
            req.context['rawBodyBytes'] = rawResult.bytes;
            if (shouldStoreRawText(contentType)) {
                req.context['rawBodyText'] = getTextFromRaw(rawResult);
            }
            else {
                req.context['rawBodyText'] = undefined;
            }
        }
    }
    else {
        await processBodyParsing(req, res, contentType, maxBytes);
    }
    await next();
};
export default bodyParsingMiddleware;
