/**
 * HTTP Kernel - Request handling and middleware pipeline
 */
import { Logger } from '../config/logger.js';
import { middlewareConfig } from '../config/middleware.js';
import { ErrorRouting } from '../routes/error.js';
import { Router } from '../routes/Router.js';
import { Request } from './Request.js';
import { RequestContext } from './RequestContext.js';
import { Response } from './Response.js';
import { MiddlewareStack } from '../middleware/MiddlewareStack.js';
import { OpenTelemetry } from '../observability/OpenTelemetry.js';
import { PrometheusMetrics } from '../observability/PrometheusMetrics.js';
import { create as createScheduleRunner } from '../scheduler/ScheduleRunner.js';
import { Env } from '../config/env.js';
/**
 * Terminate request lifecycle
 */
function terminate(_req, _res) {
    // Cleanup, logging, etc.
}
const isWritableEnded = (res) => {
    if (typeof res.getRaw !== 'function')
        return false;
    const raw = res.getRaw();
    if (typeof raw !== 'object' || raw === null)
        return false;
    if (!('writableEnded' in raw))
        return false;
    return Boolean(raw.writableEnded);
};
const getStatusSafe = (res) => {
    try {
        if (typeof res.getStatus === 'function')
            return res.getStatus();
    }
    catch {
        // ignore
    }
    try {
        if (typeof res.getRaw === 'function') {
            const raw = res.getRaw();
            const maybeStatusCode = raw.statusCode;
            if (typeof maybeStatusCode === 'number')
                return maybeStatusCode;
        }
    }
    catch {
        // ignore
    }
    return 0;
};
const resolveMiddlewareForRoute = (route, globalMiddleware, routeMiddleware) => {
    const routeAny = route;
    const routeMiddlewareNames = Array.isArray(routeAny.middleware)
        ? routeAny.middleware.filter((m) => typeof m === 'string')
        : [];
    const resolvedRouteMiddleware = routeMiddlewareNames
        .map((name) => routeMiddleware[name])
        .filter((mw) => typeof mw === 'function');
    return [...globalMiddleware, ...resolvedRouteMiddleware];
};
const PREFLIGHT_FALLBACK_METHODS = Object.freeze([
    'GET',
    'HEAD',
    'POST',
    'PUT',
    'PATCH',
    'DELETE',
    '*',
]);
const resolveRouteWithPreflightFallback = (router, method, path) => {
    const direct = Router.match(router, method, path);
    if (direct !== null || method !== 'OPTIONS')
        return direct;
    for (const fallbackMethod of PREFLIGHT_FALLBACK_METHODS) {
        const matched = Router.match(router, fallbackMethod, path);
        if (matched === null)
            continue;
        return {
            ...matched,
            handler: async () => {
                await Promise.resolve();
            },
        };
    }
    return null;
};
const maybeStartKernelTraceSpan = (req, context) => {
    if (OpenTelemetry.isEnabled() === false)
        return undefined;
    try {
        return OpenTelemetry.startHttpServerSpan(req, {
            method: context.method,
            path: req.getPath(),
            requestId: context.requestId,
            serviceName: Env.APP_NAME,
            userAgent: context.userAgent,
            userId: context.userId,
            tenantId: context.tenantId,
        });
    }
    catch {
        return undefined;
    }
};
const maybeSetKernelTraceRoute = (traceSpan, method, routeLabel) => {
    if (!traceSpan)
        return;
    OpenTelemetry.setHttpRoute(traceSpan.span, method, routeLabel);
};
const runKernelPipeline = async (router, globalMiddleware, routeMiddleware, req, res, context, traceSpan) => {
    Logger.info(`[${req.getMethod()}] ${req.getPath()}`);
    const route = resolveRouteWithPreflightFallback(router, req.getMethod(), req.getPath());
    if (route === null) {
        const routeLabel = 'not_found';
        maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
        const handleNotFound = ErrorRouting.handleNotFound;
        await handleNotFound(req, res, context.requestId);
        return routeLabel;
    }
    // Safe type guard to ensure route.routePath is a non-empty string before calling .trim()
    const hasNonEmptyRoutePath = (r) => {
        if (typeof r !== 'object' || r === null)
            return false;
        const rp = r.routePath;
        return typeof rp === 'string' && rp.trim() !== '';
    };
    const routeLabel = hasNonEmptyRoutePath(route) ? route.routePath : req.getPath();
    maybeSetKernelTraceRoute(traceSpan, context.method, routeLabel);
    // Use a typed view of the matched route to avoid unsafe member access
    const matchedRoute = route;
    // Coerce route params (which may be undefined or non-string) into the
    // expected Record<string, string> shape required by Request.setParams.
    const safeParams = {};
    if (typeof matchedRoute.params === 'object' && matchedRoute.params !== null) {
        for (const [k, v] of Object.entries(matchedRoute.params)) {
            if (v === undefined || v === null)
                continue;
            safeParams[k] = typeof v === 'string' ? v : String(v);
        }
    }
    req.setParams(safeParams);
    const middlewareToRun = resolveMiddlewareForRoute(matchedRoute, globalMiddleware, routeMiddleware);
    let index = 0;
    const next = async () => {
        if (index < middlewareToRun.length) {
            const mw = middlewareToRun[index++];
            await mw(req, res, next);
            return;
        }
        await matchedRoute.handler(req, res);
    };
    await next();
    return routeLabel;
};
const finalizeKernelObservability = (context, res, routeLabel, thrown, traceSpan) => {
    if (Env.getBool('METRICS_ENABLED', false)) {
        const status = getStatusSafe(res);
        const durationMs = Date.now() - context.startTime;
        void PrometheusMetrics.observeHttpRequest({
            method: context.method,
            route: routeLabel,
            status,
            durationMs,
        }).catch(() => {
            // best-effort
        });
    }
    if (traceSpan) {
        const status = getStatusSafe(res);
        // Late-bind context-derived attributes so auth/tenant middleware can populate them.
        try {
            if (typeof context.userId === 'string' && context.userId.trim() !== '') {
                traceSpan.span.setAttribute('enduser.id', context.userId);
            }
            if (typeof context.tenantId === 'string' && context.tenantId.trim() !== '') {
                traceSpan.span.setAttribute('zintrust.tenant_id', context.tenantId);
            }
            if (typeof context.traceId === 'string' && context.traceId.trim() !== '') {
                traceSpan.span.setAttribute('zintrust.trace_id', context.traceId);
            }
        }
        catch {
            // best-effort
        }
        OpenTelemetry.endHttpServerSpan(traceSpan.span, {
            route: routeLabel,
            status,
            error: thrown,
        });
    }
};
const createHandleRequest = (router, globalMiddleware, routeMiddleware) => {
    return async (req, res) => {
        const context = RequestContext.create(req);
        let routeLabel = 'unmatched';
        let traceSpan;
        let thrown;
        try {
            await RequestContext.run(context, async () => {
                traceSpan = maybeStartKernelTraceSpan(req, context);
                const run = async () => runKernelPipeline(router, globalMiddleware, routeMiddleware, req, res, context, traceSpan);
                routeLabel = traceSpan
                    ? await OpenTelemetry.runWithContext(traceSpan.context, run)
                    : await run();
            });
        }
        catch (error) {
            thrown = error;
            Logger.error('Kernel error:', error);
            if (!isWritableEnded(res)) {
                ErrorRouting.handleInternalServerErrorWithWrappers(req, res, error, context.requestId);
            }
        }
        finally {
            finalizeKernelObservability(context, res, routeLabel, thrown, traceSpan);
            terminate(req, res);
        }
    };
};
const createHandle = (handleRequest) => async (nodeReq, nodeRes) => {
    const req = Request.create(nodeReq);
    const res = Response.create(nodeRes);
    await handleRequest(req, res);
};
/**
 * HTTP Kernel Factory
 */
const create = (router, container) => {
    const globalMiddleware = [];
    const routeMiddleware = {};
    const middlewareStack = MiddlewareStack.create();
    // Scheduling runner (for long-running runtimes)
    const scheduleRunner = createScheduleRunner();
    const scheduleKernel = Object.freeze({
        getContainer: () => container,
        getRouter: () => router,
    });
    // Register default middleware config
    globalMiddleware.push(...middlewareConfig.global);
    for (const [name, mw] of Object.entries(middlewareConfig.route)) {
        routeMiddleware[name] = mw;
    }
    const handleRequest = createHandleRequest(router, globalMiddleware, routeMiddleware);
    const handle = createHandle(handleRequest);
    return {
        handle,
        handleRequest,
        terminate,
        registerGlobalMiddleware(...middleware) {
            globalMiddleware.push(...middleware);
        },
        registerRouteMiddleware(name, middleware) {
            routeMiddleware[name] = middleware;
        },
        getRouter: () => router,
        getContainer: () => container,
        getMiddlewareStack: () => middlewareStack,
        // Scheduling API
        registerSchedule(schedule) {
            scheduleRunner.register(schedule);
        },
        startSchedules() {
            // Delegated to the internal ScheduleRunner. Kernel will call startSchedules
            // during boot for long-running runtimes (Node / Fargate).
            scheduleRunner.start(scheduleKernel);
        },
        async stopSchedules() {
            const timeoutMs = Env.getInt('SCHEDULE_SHUTDOWN_TIMEOUT_MS', 30000);
            await scheduleRunner.stop(timeoutMs);
        },
        async runScheduleOnce(name) {
            await scheduleRunner.runOnce(name, scheduleKernel);
        },
    };
};
export const Kernel = Object.freeze({
    create,
});
export default Kernel;
