import { getClientIp } from 'request-ip';
import cleanStack from 'clean-stack';
import { ServiceError } from '../error.js';
import { getNodeEnv } from '../env.js';
const LOG_PREFS = Symbol('Logging information');
const LOGGED_SEMAPHORE = Symbol('Logged semaphore');
function getBasicInfo(req) {
    const url = req.originalUrl || req.url;
    const preInfo = {
        ip: getClientIp(req) || '',
        m: req.method,
    };
    if (req.headers['user-agent']) {
        preInfo.ua = req.headers['user-agent'];
    }
    const sessionReq = req;
    if (sessionReq.session?.id) {
        preInfo.sid = sessionReq.session.id;
    }
    return [url, preInfo];
}
function finishLog(app, error, req, res, histogram) {
    if (res[LOGGED_SEMAPHORE]) {
        return;
    }
    const prefs = res.locals[LOG_PREFS] || {};
    if (prefs.logged) {
        // This happens when error handler runs, but onEnd hasn't fired yet. We only log the first one.
        return;
    }
    const { logger, service } = app.locals;
    let dur = 0;
    if (prefs.start) {
        const hrdur = process.hrtime(prefs.start);
        dur = hrdur[0] + hrdur[1] / 1000000000;
    }
    const [url, preInfo] = getBasicInfo(req);
    let responseType = 'unknown';
    if (res.writableFinished) {
        responseType = 'finished';
    }
    else if (req.aborted) {
        responseType = 'aborted';
    }
    else if (req.destroyed) {
        responseType = 'destroyed';
    }
    else if (error) {
        responseType = 'errored';
    }
    const endLog = {
        ...preInfo,
        t: 'req',
        r: responseType,
        s: error?.status || res.statusCode || 0,
        dur,
    };
    const path = req.route ? { path: req.route.path } : undefined;
    histogram.record(dur, {
        status_code: endLog.s,
        method: endLog.m,
        ...path,
        service: app.locals.name,
    });
    if (res.locals.user?.id) {
        endLog.u = res.locals.user.id;
    }
    let unexpectedError = false;
    if (error) {
        endLog.e = error.message;
        if (!(error instanceof ServiceError) || error.log_stack) {
            endLog.st = cleanStack(error.stack);
        }
        if (!error.expected_error) {
            unexpectedError = true;
        }
    }
    if (prefs.logRequests) {
        endLog.h = JSON.stringify(req.headers);
        if (Buffer.isBuffer(req.body)) {
            endLog.b = req.body.toString('base64');
        }
        else if (typeof req.body !== 'string') {
            endLog.b = JSON.stringify(req.body);
        }
        else if (req.body) {
            endLog.b = req.body;
        }
    }
    if (prefs.chunks?.length) {
        const bodyString = Buffer.concat(prefs.chunks).toString('utf8');
        if (bodyString) {
            endLog.resBody = bodyString;
        }
    }
    const msg = service.getLogFields?.(req, endLog) || url;
    if (unexpectedError) {
        logger.error(endLog, msg);
    }
    else {
        logger.info(endLog, msg);
    }
    res[LOGGED_SEMAPHORE] = true;
}
export function loggerMiddleware(app, histogram, config) {
    const nonProd = getNodeEnv() !== 'production';
    const { logger, service } = app.locals;
    return function serviceLogger(req, res, next) {
        const logResponse = config?.logResponseBody || (nonProd && req.headers['x-log']?.includes('res'));
        const logRequest = config?.logRequestBody || (nonProd && req.headers['x-log']?.includes('req'));
        const prefs = {
            start: process.hrtime(),
            logRequests: logRequest,
            chunks: logResponse ? [] : undefined,
            logged: false,
        };
        res.locals[LOG_PREFS] = prefs;
        if (logResponse) {
            // res is a read-only stream, so the only way to intercept response
            // data is to monkey-patch.
            const oldWrite = res.write;
            const oldEnd = res.end;
            res.write = ((...args) => {
                if (prefs.chunks) {
                    prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]));
                }
                return oldWrite.apply(res, args);
            });
            res.end = ((...args) => {
                if (args[0] && prefs.chunks) {
                    prefs.chunks.push(Buffer.isBuffer(args[0]) ? args[0] : Buffer.from(args[0]));
                }
                return oldEnd.apply(res, args);
            });
        }
        if (config?.preLog) {
            const [url, preInfo] = getBasicInfo(req);
            const preLog = {
                ...preInfo,
                t: 'pre',
                ref: req.headers.referer || undefined,
                sid: req.session?.id,
                c: req.headers.correlationid || undefined,
            };
            const msg = service.getLogFields?.(req, preLog) || url;
            logger.info(preLog, msg);
        }
        const logWriter = (err) => finishLog(app, err, req, res, histogram);
        res.on('finish', logWriter);
        res.on('close', logWriter);
        res.on('error', logWriter);
        next();
    };
}
export function errorHandlerMiddleware(app, histogram, unnest, returnError) {
    const svcErrorHandler = (error, req, res, next) => {
        let loggable = error;
        const body = error.response?.body || error.body;
        if (unnest && body?.domain && body?.code && body?.message) {
            loggable = {
                status: error.status,
                message: body.message,
                domain: body.domain,
                code: body.code,
                display_message: body.display_message,
                ...loggable.client_metadata,
            };
        }
        // Set the status to error, even if we aren't going to render the error.
        res.status(loggable.status || 500);
        if (returnError) {
            finishLog(app, error, req, res, histogram);
            const prefs = res.locals[LOG_PREFS];
            prefs.logged = true;
            res.json({
                code: loggable.code,
                message: loggable.message,
                domain: loggable.domain,
                display_message: loggable.display_message,
                ...loggable.client_metadata,
            });
        }
        else {
            next(error);
        }
    };
    return svcErrorHandler;
}
export function notFoundMiddleware() {
    const serviceNotFoundHandler = (req, res, next) => {
        const error = new ServiceError(req.app, `Cannot ${req.method} ${req.path}`, {
            status: 404,
            code: 'NotFound',
            domain: 'http',
        });
        next(error);
    };
    return serviceNotFoundHandler;
}
//# sourceMappingURL=requestLogger.js.map