import assert from 'assert';
import http from 'http';
import https from 'https';
import path from 'path';
import { destination, pino } from 'pino';
import cookieParser from 'cookie-parser';
import { context, metrics, trace } from '@opentelemetry/api';
import { setupNodeMetrics } from '@sesamecare-oss/opentelemetry-node-metrics';
import { createTerminus } from '@godaddy/terminus';
import { loadConfiguration } from '../config/index.js';
import { openApi } from '../openapi.js';
import { errorHandlerMiddleware, loggerMiddleware, notFoundMiddleware, } from '../telemetry/requestLogger.js';
import { shortstops } from '../config/shortstops.js';
import { getNodeEnv, isDev } from '../env.js';
import { getGlobalPrometheusExporter } from '../telemetry/index.js';
import { loadRoutes } from './route-loader.js';
import { startInternalApp } from './internal-server.js';
function isSyncLogging() {
    if (process.env.LOG_SYNC) {
        return true;
    }
    return isDev() || getNodeEnv() === 'test';
}
export async function startApp(startOptions) {
    const { service, rootDirectory, codepath = 'build', name, version } = startOptions;
    const shouldPrettyPrint = isDev() && !process.env.NO_PRETTY_LOGS;
    const pinoDestination = destination({
        sync: isSyncLogging(),
        dest: process.env.LOG_TO_FILE || process.stdout.fd,
        minLength: process.env.LOG_BUFFER ? Number(process.env.LOG_BUFFER) : undefined,
    });
    function poorMansOtlp(mergeObject) {
        if (!('trace_id' in mergeObject)) {
            const activeSpan = trace.getSpan(context.active());
            if (activeSpan) {
                const ctx = activeSpan.spanContext();
                Object.assign(mergeObject, {
                    trace_id: ctx.traceId,
                    span_id: ctx.spanId,
                    trace_flags: ctx.traceFlags,
                });
            }
        }
        return mergeObject;
    }
    const logger = shouldPrettyPrint
        ? pino({
            transport: {
                target: 'pino-pretty',
                options: {
                    colorize: true,
                },
            },
            mixin: poorMansOtlp,
        }, pinoDestination)
        : pino({
            formatters: {
                level(label) {
                    return { level: label };
                },
            },
            mixin: poorMansOtlp,
        }, pinoDestination);
    const serviceImpl = service();
    assert(serviceImpl?.start, 'Service function did not return a conforming object');
    const sourceDirectory = path.join(rootDirectory, codepath);
    const codeExtension = codepath === 'src' ? '.ts' : '.js';
    const baseOptions = {
        configurationDirectories: [path.resolve(rootDirectory, './config')],
        shortstopHandlers: shortstops({ name }, sourceDirectory),
        codepath,
        codeExtension,
    };
    const options = serviceImpl.configure?.(startOptions, baseOptions) || baseOptions;
    const config = await loadConfiguration({
        configurationDirectories: options.configurationDirectories,
        shortstopHandlers: options.shortstopHandlers,
    });
    const logging = config.logging;
    logger.level = logging?.level || 'info';
    // Concentrate the Typescript ugliness...
    const { default: express } = await import('express');
    const app = express();
    const routing = config.routing;
    app.disable('x-powered-by');
    if (routing?.etag !== true) {
        app.disable('etag');
    }
    Object.assign(app.locals, startOptions.locals, {
        service: serviceImpl,
        logger,
        config,
        name,
        version,
    });
    if (serviceImpl.attach) {
        await serviceImpl.attach(app);
    }
    app.locals.meter = metrics.getMeterProvider().getMeter(name);
    setupNodeMetrics(app.locals.meter, {});
    if (config.trustProxy === true) {
        app.enable('trust proxy');
    }
    else if (config.trustProxy) {
        app.set('trust proxy', config.trustProxy);
    }
    const histogram = app.locals.meter.createHistogram('http_request_duration_seconds', {
        description: 'Duration of HTTP requests in seconds',
    });
    app.use(loggerMiddleware(app, histogram, logging));
    // Allow the service to add locals, etc. We put this before the body parsers
    // so that the req can decide whether to save the raw request body or not.
    const attachServiceLocals = (req, res, next) => {
        res.locals.logger = logger;
        try {
            const result = serviceImpl.onRequest?.(req, res);
            if (result !== undefined && result !== null && typeof result.then === 'function') {
                void result.catch(next).then(next);
            }
            else {
                next();
            }
        }
        catch (error) {
            next(error);
        }
    };
    app.use(attachServiceLocals);
    if (routing?.cookieParser) {
        app.use(cookieParser());
    }
    if (routing?.bodyParsers?.json) {
        const jsonArgs = typeof routing.bodyParsers.json === 'object' ? routing.bodyParsers.json : {};
        app.use(express.json({
            verify(req, res, buf) {
                const locals = res.locals;
                if (locals?.rawBody === true) {
                    locals.rawBody = buf;
                }
            },
            ...jsonArgs,
        }));
    }
    if (routing?.bodyParsers?.form) {
        app.use(express.urlencoded(typeof routing.bodyParsers.form === 'object' ? routing.bodyParsers.form : {}));
    }
    if (serviceImpl.authorize) {
        const authorize = (req, res, next) => {
            let maybePromise;
            try {
                maybePromise = serviceImpl.authorize?.(req, res);
            }
            catch (error) {
                next(error);
            }
            if (maybePromise && typeof maybePromise !== 'boolean') {
                maybePromise
                    .then((val) => {
                    if (val === false) {
                        return;
                    }
                    next();
                })
                    .catch(next);
            }
            else if (maybePromise !== false) {
                next();
            }
        };
        app.use(authorize);
    }
    if (routing?.static?.enabled) {
        const localdir = path.resolve(rootDirectory, routing?.static?.path || 'public');
        if (routing.static.mountPath) {
            app.use(routing.static.mountPath, express.static(localdir));
        }
        else {
            app.use(express.static(localdir));
        }
    }
    if (routing?.freezeQuery) {
        app.use(function freezeQuery(req, res, next) {
            // Express 5 re-parses the query string every time. This causes problems with
            // various libraries, namely the express OpenAPI parser. So we "freeze it" in place
            // here, which runs right before the routing validation logic does. Note that this
            // means the app middleware will see the unfrozen one, which is intentional. If the
            // app wants to modify or freeze the query itself, this shouldn't get in the way.
            const { query } = req;
            if (query) {
                Object.defineProperty(req, 'query', {
                    configurable: true,
                    enumerable: true,
                    value: query,
                });
            }
            next();
        });
    }
    const codePattern = codepath === 'src' ? '**/*.ts' : '**/*.js';
    if (routing?.routes) {
        await loadRoutes(app, path.resolve(rootDirectory, codepath, config.routing?.routes || 'routes'), codePattern);
    }
    if (routing?.openapi) {
        const openApiMiddleware = await openApi(app, rootDirectory, codepath, codePattern, options.openApiOptions);
        app.use(openApiMiddleware);
    }
    // Putting this here allows more flexible middleware insertion
    await serviceImpl.start(app);
    const { notFound, errors } = routing?.finalHandlers || {};
    if (notFound) {
        app.use(notFoundMiddleware());
    }
    if (errors?.enabled) {
        app.use(errorHandlerMiddleware(app, histogram, errors?.unnest, errors?.render));
    }
    return app;
}
export async function shutdownApp(app) {
    const { logger } = app.locals;
    try {
        await app.locals.service.stop?.(app);
        logger.info('App shutdown complete');
    }
    catch (error) {
        logger.warn(error, 'Shutdown failed');
    }
    logger.flush?.();
}
function httpServer(app, config) {
    if (!config.certificate) {
        return http.createServer(app);
    }
    return https.createServer({
        key: config.key ? Buffer.from(config.key) : undefined,
        cert: config.certificate ? Buffer.from(config.certificate) : undefined,
    }, app);
}
function url(config, port) {
    if (config.certificate) {
        return `https://${config.hostname}${port === 443 ? '' : `:${port}`}`;
    }
    return `http://${config.hostname}${port === 80 ? '' : `:${port}`}`;
}
export async function listen(app, shutdownHandler) {
    const config = app.locals.config.server || {};
    const { port } = config;
    const { service, logger } = app.locals;
    const server = httpServer(app, config);
    await app.locals.service.attachServer?.(app, server);
    let shutdownInProgress = false;
    createTerminus(server, {
        timeout: 15000,
        useExit0: true,
        // https://github.com/godaddy/terminus#how-to-set-terminus-up-with-kubernetes
        beforeShutdown() {
            if (shutdownInProgress) {
                return Promise.resolve();
            }
            shutdownInProgress = true;
            if (app.locals.internalApp) {
                app.locals.internalApp.locals.server?.close();
            }
            logger.info('Graceful shutdown beginning');
            return new Promise((accept) => {
                // Per docs https://www.npmjs.com/package/@godaddy/terminus in Kubernetes, wait for readiness threshold
                setTimeout(accept, 10000);
            });
        },
        onShutdown() {
            return Promise.resolve()
                .then(() => service.stop?.(app))
                .then(() => {
                logger.info('Service stop complete');
            })
                .then(shutdownHandler || (() => Promise.resolve()))
                .then(() => logger.info('Graceful shutdown complete'))
                .catch((error) => logger.error(error, 'Error terminating tracing'))
                .then(() => logger.flush?.());
        },
        logger: (msg, e) => {
            logger.error(e, msg);
        },
    });
    server.on('close', () => {
        if (!shutdownInProgress) {
            shutdownInProgress = true;
            app.locals.logger.info('Shutdown requested');
            if (app.locals.internalApp) {
                if (app.locals.internalApp.locals.server?.listening) {
                    app.locals.internalApp.locals.server?.close();
                }
            }
            void shutdownApp(app);
        }
    });
    server.on('error', (error) => {
        logger.error(error, 'Main service listener error');
    });
    // TODO handle rejection/error?
    const listenPromise = new Promise((accept) => {
        server.listen(port, () => {
            const { locals } = app;
            locals.logger.info({ url: url(config, port), service: locals.name }, 'express listening');
            const serverConfig = app.locals.config.server;
            // Ok now start the internal port if we have one.
            if (serverConfig?.internalPort || serverConfig?.internalPort === 0) {
                startInternalApp(app, serverConfig.internalPort)
                    .then((internalApp) => {
                    locals.internalApp = internalApp;
                    const prometheusExporter = getGlobalPrometheusExporter();
                    if (prometheusExporter) {
                        locals.internalApp.get('/metrics', prometheusExporter.getMetricsRequestHandler.bind(prometheusExporter));
                        locals.logger.info('Metrics exporter started');
                    }
                    else {
                        locals.logger.info('No metrics will be exported');
                    }
                    if (app.locals.openApiSpecification) {
                        locals.internalApp.get('/api-docs', (req, res) => {
                            res.json(app.locals.openApiSpecification);
                        });
                    }
                    accept();
                })
                    .catch((error) => {
                    locals.logger.warn(error, 'Failed to start internal metadata app');
                });
            }
            else {
                accept();
            }
        });
    });
    await listenPromise;
    await service.onListening?.(app, { port, protocol: config.certificate ? 'https' : 'http' });
    return server;
}
//# sourceMappingURL=app.js.map