/**
 * @fileoverview Application composition root. Constructs all services in
 * dependency order, initializes telemetry/logger, starts transport, and
 * registers shutdown/signal handlers. Returns a ServerHandle for lifecycle
 * management.
 * @module src/core/app
 */
import { z } from 'zod';
import { config, resetConfig } from '../config/index.js';
import { startGcPressureLoop } from '../core/gcPressure.js';
import { buildServerManifest, } from '../core/serverManifest.js';
import { PromptRegistry } from '../mcp-server/prompts/prompt-registration.js';
import { ResourceRegistry } from '../mcp-server/resources/resource-registration.js';
import { createMcpServerInstance } from '../mcp-server/server.js';
import { TaskManager } from '../mcp-server/tasks/core/taskManager.js';
import { isTaskToolDefinition } from '../mcp-server/tasks/utils/taskToolDefinition.js';
import { ToolRegistry } from '../mcp-server/tools/tool-registration.js';
import { initHeartbeatMetrics } from '../mcp-server/transports/heartbeat.js';
import { initSessionMetrics } from '../mcp-server/transports/http/sessionStore.js';
import { TransportManager } from '../mcp-server/transports/manager.js';
import { createCanvasService } from '../services/canvas/core/canvasFactory.js';
import { OpenRouterProvider } from '../services/llm/providers/openrouter.provider.js';
import { SpeechService } from '../services/speech/core/SpeechService.js';
import { StorageService } from '../storage/core/StorageService.js';
import { createStorageProvider } from '../storage/core/storageFactory.js';
import { configurationError, JsonRpcErrorCode, McpError } from '../types-global/errors.js';
import { initErrorMetrics } from '../utils/internal/error-handler/errorHandler.js';
import { logger } from '../utils/internal/logger.js';
import { initHandlerMetrics } from '../utils/internal/performance.js';
import { requestContextService } from '../utils/internal/requestContext.js';
import { initHttpClientMetrics } from '../utils/network/fetchWithTimeout.js';
import { schedulerService } from '../utils/scheduling/scheduler.js';
import { initRateLimitMetrics, RateLimiter } from '../utils/security/rateLimiter.js';
import { initializeOpenTelemetry, shutdownOpenTelemetry, } from '../utils/telemetry/instrumentation.js';
import { createObservableGauge } from '../utils/telemetry/metrics.js';
import { withSpan } from '../utils/telemetry/trace.js';
/**
 * Constructs core services, registries, and server factory.
 * Shared by createApp() (Node) and createWorkerHandler() (Workers).
 * Does NOT start transport, register signal handlers, or init OTEL/logger.
 * @internal
 */
export async function composeServices(options = {}) {
    const { tools = [], resources = [], prompts = [], description, extensions, icons, instructions, landing, setup, title, websiteUrl, context: contextOptions, } = options;
    // Persist name/version overrides to process.env so they survive resetConfig()
    // and are visible to OTEL, logger, and transport throughout the process lifetime.
    if (options.name || options.version) {
        if (typeof process !== 'undefined' && process.env) {
            if (options.name) {
                process.env.MCP_SERVER_NAME = options.name;
                process.env.OTEL_SERVICE_NAME ??= options.name;
            }
            if (options.version) {
                process.env.MCP_SERVER_VERSION = options.version;
                process.env.OTEL_SERVICE_VERSION ??= options.version;
            }
        }
        resetConfig();
    }
    // --- Core services ---
    let supabaseClient;
    if (config.storage.providerType === 'supabase') {
        if (!config.supabase?.url || !config.supabase?.serviceRoleKey) {
            throw configurationError('Supabase URL or service role key is missing for admin client.');
        }
        const { createClient } = await import('@supabase/supabase-js').catch(() => {
            throw configurationError('Install "@supabase/supabase-js" to use Supabase storage: bun add @supabase/supabase-js');
        });
        supabaseClient = createClient(config.supabase.url, config.supabase.serviceRoleKey, {
            auth: { persistSession: false, autoRefreshToken: false },
        });
    }
    const storageProvider = createStorageProvider(config, {
        ...(supabaseClient && { supabaseClient }),
    });
    const storageService = new StorageService(storageProvider);
    const rateLimiter = new RateLimiter(config, logger);
    // --- Optional services (constructed when config is present) ---
    let llmProvider;
    if (config.openrouterApiKey) {
        llmProvider = new OpenRouterProvider(rateLimiter, config, logger);
    }
    let speechService;
    if (config.speech) {
        const ttsConfig = config.speech.tts?.enabled
            ? config.speech.tts
            : undefined;
        const sttConfig = config.speech.stt?.enabled
            ? config.speech.stt
            : undefined;
        if (ttsConfig || sttConfig) {
            speechService = new SpeechService(ttsConfig, sttConfig);
        }
    }
    const canvas = createCanvasService(config);
    const coreServices = {
        config,
        logger,
        rateLimiter,
        storage: storageService,
        ...(canvas && { canvas }),
        ...(llmProvider && { llmProvider }),
        ...(speechService && { speechService }),
        ...(supabaseClient && { supabase: supabaseClient }),
    };
    // --- Server-specific setup ---
    // If the server's setup() throws a raw ZodError (e.g., from a
    // ServerConfigSchema.parse() call without framework helpers), convert it to
    // a ConfigurationError so downstream handlers can format it cleanly instead
    // of dumping a raw stack trace at process start.
    if (setup) {
        try {
            await setup(coreServices);
        }
        catch (err) {
            if (err instanceof z.ZodError) {
                const lines = err.issues.map((issue) => {
                    const path = issue.path.join('.');
                    return `  - ${path}: ${issue.message}`;
                });
                throw configurationError(`Server setup failed — config validation error:\n${lines.join('\n')}`, { issues: err.issues }, { cause: err });
            }
            throw err;
        }
    }
    // --- MCP services ---
    const taskManager = new TaskManager(config, storageService);
    const advertiseTasks = tools.some((def) => isTaskToolDefinition(def) || def.task === true);
    const exposeStatelessSessionId = contextOptions?.exposeStatelessSessionId === true;
    const toolRegistry = new ToolRegistry(tools, {
        logger,
        storage: storageService,
        exposeStatelessSessionId,
    });
    const resourceRegistry = new ResourceRegistry(resources, {
        logger,
        storage: storageService,
        exposeStatelessSessionId,
    });
    const promptRegistry = new PromptRegistry(prompts, logger);
    const createServer = () => createMcpServerInstance({
        advertiseTasks,
        config,
        ...(description && { description }),
        ...(extensions && { extensions }),
        ...(icons && { icons }),
        ...(instructions && { instructions }),
        ...(title && { title }),
        ...(websiteUrl && { websiteUrl }),
        promptRegistry,
        resourceRegistry,
        taskStore: taskManager.getTaskStore(),
        taskMessageQueue: taskManager.getMessageQueue(),
        toolRegistry,
    });
    const manifest = buildServerManifest({
        config,
        tools,
        resources,
        prompts,
        ...(description && { description }),
        ...(extensions && { extensions }),
        ...(icons && { icons }),
        ...(landing && { landing }),
        ...(title && { title }),
        ...(websiteUrl && { websiteUrl }),
    });
    return {
        coreServices,
        createServer,
        manifest,
        taskManager,
    };
}
// ---------------------------------------------------------------------------
// Public: createApp()
// ---------------------------------------------------------------------------
/**
 * Suppresses ANSI color codes for MCP client compatibility.
 */
function suppressColors() {
    const transportType = process.env.MCP_TRANSPORT_TYPE?.toLowerCase();
    const isStdioMode = !transportType || transportType === 'stdio';
    const isHttpModeWithoutTty = transportType === 'http' && !process.stdout.isTTY;
    if (isStdioMode || isHttpModeWithoutTty) {
        process.env.NO_COLOR = '1';
        process.env.FORCE_COLOR = '0';
    }
}
/**
 * Writes a user-friendly banner to stderr for startup-time configuration errors,
 * hiding the framework stack trace behind `DEBUG=true`. Meant to replace the raw
 * ZodError/stack dump that Node prints for unhandled rejections during `createApp`.
 */
function printStartupConfigError(err) {
    const isDebug = !!process.env.DEBUG;
    const divider = '-'.repeat(60);
    const lines = [
        '',
        divider,
        ' Configuration error — server failed to start',
        divider,
        err.message,
        '',
        'Check your .env file or environment variables, then retry.',
    ];
    if (!isDebug) {
        lines.push('Set DEBUG=true for the full stack trace.');
    }
    lines.push(divider, '');
    process.stderr.write(`${lines.join('\n')}\n`);
    if (isDebug) {
        if (err.data) {
            process.stderr.write(`Data: ${JSON.stringify(err.data, null, 2)}\n`);
        }
        if (err.stack) {
            process.stderr.write(`${err.stack}\n`);
        }
    }
}
/**
 * Composes the application, initializes all services, starts transport,
 * and registers signal/error handlers. This is the complete entry point
 * for a Node.js MCP server process.
 *
 * @param options - Definition arrays, name/version overrides, and setup callback.
 *                  All arrays default to empty.
 * @returns A {@link ServerHandle} with `shutdown()` and `services`.
 * @throws {McpError} If config parsing or service construction fails.
 */
export async function createApp(options = {}) {
    suppressColors();
    // --- Compose services (handles env overrides internally for config parsing) ---
    // Configuration errors at this stage (from server setup() or missing required
    // env vars) are startup-fatal. Print a clean banner and exit rather than
    // letting the raw error dump to stderr.
    let composed;
    try {
        composed = await composeServices(options);
    }
    catch (err) {
        if (err instanceof McpError && err.code === JsonRpcErrorCode.ConfigurationError) {
            printStartupConfigError(err);
            process.exit(1);
        }
        throw err;
    }
    const { coreServices, createServer, manifest, taskManager } = composed;
    const { definitionCounts } = manifest;
    // --- Initialize OTEL ---
    await initializeOpenTelemetry().catch((err) => {
        console.error('[Startup] Failed to initialize OpenTelemetry:', err);
    });
    // --- Eager-init universal metrics so series exist from first export cycle ---
    initHeartbeatMetrics();
    initSessionMetrics();
    initErrorMetrics();
    initRateLimitMetrics();
    initHttpClientMetrics();
    initHandlerMetrics();
    // --- Process-level observable gauges (registered once after OTEL init) ---
    if (typeof process !== 'undefined' && typeof process.memoryUsage === 'function') {
        // Share a single memoryUsage() syscall across the three gauge callbacks per collection cycle. The snapshot is refreshed at most once per 100 ms.
        let memSnapshot;
        let memSnapshotTs = 0;
        const getMemSnapshot = () => {
            const now = Date.now();
            if (!memSnapshot || now - memSnapshotTs > 100) {
                memSnapshot = process.memoryUsage();
                memSnapshotTs = now;
            }
            return memSnapshot;
        };
        createObservableGauge('process.memory.rss', 'Process resident set size', () => getMemSnapshot().rss, 'bytes');
        createObservableGauge('process.memory.heap_used', 'V8 heap memory used', () => getMemSnapshot().heapUsed, 'bytes');
        createObservableGauge('process.memory.heap_total', 'V8 total heap size', () => getMemSnapshot().heapTotal, 'bytes');
    }
    if (typeof process !== 'undefined' && typeof process.uptime === 'function') {
        createObservableGauge('process.uptime', 'Process uptime in seconds', () => process.uptime(), 's');
    }
    // --- Event loop gauges (delay + utilization) ---
    if (typeof process !== 'undefined') {
        try {
            const { monitorEventLoopDelay, performance } = await import('node:perf_hooks');
            const eld = monitorEventLoopDelay({ resolution: 20 });
            eld.enable();
            createObservableGauge('process.event_loop.delay', 'Event loop delay p99 in milliseconds', () => eld.percentile(99) / 1e6, // ns → ms
            'ms');
            let prevELU = performance.eventLoopUtilization();
            createObservableGauge('process.event_loop.utilization', 'Event loop utilization ratio (0 = idle, 1 = saturated)', () => {
                const current = performance.eventLoopUtilization();
                const delta = performance.eventLoopUtilization(prevELU, current);
                prevELU = current;
                return delta.utilization;
            }, '1');
        }
        catch {
            // perf_hooks unavailable in this runtime — skip silently
        }
    }
    // --- Initialize logger ---
    await logger.initialize(config.logLevel, config.mcpTransportType);
    // --- Optional forced-GC pressure loop (issue #50 mitigation) ---
    const stopGcPressure = startGcPressureLoop(config.mcpGcPressureIntervalMs);
    logger.info(`Core services constructed — ${definitionCounts.tools} tool(s), ${definitionCounts.resources} resource(s), ${definitionCounts.prompts} prompt(s). Storage: ${config.storage.providerType}.`, requestContextService.createRequestContext({
        operation: 'ServerInit',
        additionalContext: {
            tools: (options.tools ?? []).map((t) => t.name),
            resources: (options.resources ?? []).map((r) => r.name ?? r.uriTemplate),
            prompts: (options.prompts ?? []).map((p) => p.name),
        },
    }));
    // --- Transport ---
    const transportManager = new TransportManager(config, logger, createServer, taskManager, manifest);
    // --- Startup context ---
    const startupContext = requestContextService.createRequestContext({
        operation: 'ServerStartup',
        applicationName: config.mcpServerName,
        applicationVersion: config.mcpServerVersion,
        nodeEnvironment: config.environment,
    });
    logger.info(`Starting ${config.mcpServerName} (v${config.mcpServerVersion})...`, startupContext);
    // --- Shutdown ---
    let isShuttingDown = false;
    const flushTelemetryAndLogger = async () => {
        try {
            await shutdownOpenTelemetry();
            await logger.close();
        }
        catch {
            // Ignore errors during final cleanup
        }
    };
    const shutdown = async (signal = 'SHUTDOWN') => {
        if (isShuttingDown)
            return;
        isShuttingDown = true;
        // Remove listeners to prevent re-entry and allow GC in tests
        process.removeListener('uncaughtException', onUncaughtException);
        process.removeListener('unhandledRejection', onUnhandledRejection);
        process.removeListener('SIGTERM', onSigterm);
        process.removeListener('SIGINT', onSigint);
        const shutdownContext = requestContextService.createRequestContext({
            operation: 'ServerShutdown',
            triggerEvent: signal,
        });
        logger.info(`Received ${signal}. Initiating graceful shutdown...`, shutdownContext);
        try {
            await withSpan('mcp.server.shutdown', async (span) => {
                span.setAttribute('mcp.server.shutdown.signal', signal);
                await withSpan('mcp.server.shutdown.transport', async () => {
                    await transportManager.stop(signal);
                });
                taskManager.cleanup();
                coreServices.rateLimiter.dispose();
                schedulerService.destroyAll();
                stopGcPressure();
                if (coreServices.canvas) {
                    await coreServices.canvas.shutdown(shutdownContext).catch((err) => {
                        logger.warning('Canvas shutdown raised — continuing.', {
                            ...shutdownContext,
                            error: err instanceof Error ? err.message : String(err),
                        });
                    });
                }
                logger.info('Graceful shutdown completed successfully.', shutdownContext);
            });
            await flushTelemetryAndLogger();
        }
        catch (error) {
            const err = error instanceof Error ? error : new Error(String(error));
            logger.error('Critical error during shutdown process.', err, shutdownContext);
            await flushTelemetryAndLogger();
        }
    };
    // --- Named signal/error handlers (stored for cleanup in shutdown) ---
    const fatalShutdown = (signal) => {
        const backstop = setTimeout(() => process.exit(1), 10_000);
        backstop.unref();
        void shutdown(signal).finally(() => process.exit(1));
    };
    const onUncaughtException = (error) => {
        const fatalContext = requestContextService.createRequestContext({
            operation: 'FatalError',
            triggerEvent: 'uncaughtException',
        });
        logger.fatal('FATAL: Uncaught exception detected.', error, fatalContext);
        fatalShutdown('uncaughtException');
    };
    const onUnhandledRejection = (reason) => {
        const err = reason instanceof Error ? reason : new Error(String(reason));
        const fatalContext = requestContextService.createRequestContext({
            operation: 'FatalError',
            triggerEvent: 'unhandledRejection',
        });
        logger.fatal('FATAL: Unhandled promise rejection detected.', err, fatalContext);
        fatalShutdown('unhandledRejection');
    };
    const onSigterm = () => void shutdown('SIGTERM');
    const onSigint = () => void shutdown('SIGINT');
    // --- Register error/signal handlers (before transport start so a SIGTERM
    //     during HTTP bind still triggers graceful shutdown) ---
    process.on('uncaughtException', onUncaughtException);
    process.on('unhandledRejection', onUnhandledRejection);
    process.on('SIGTERM', onSigterm);
    process.on('SIGINT', onSigint);
    // --- Start transport (wrapped in startup span with phase breakdown) ---
    await withSpan('mcp.server.startup', async (span) => {
        span.setAttribute('mcp.server.name', config.mcpServerName);
        span.setAttribute('mcp.server.version', config.mcpServerVersion);
        span.setAttribute('mcp.server.transport', config.mcpTransportType);
        span.setAttribute('mcp.server.tools_count', definitionCounts.tools);
        span.setAttribute('mcp.server.resources_count', definitionCounts.resources);
        span.setAttribute('mcp.server.prompts_count', definitionCounts.prompts);
        await withSpan('mcp.server.startup.transport', async () => {
            await transportManager.start();
        });
    });
    logger.info(`${config.mcpServerName} is now running and ready.`, startupContext);
    return { services: coreServices, shutdown };
}
// ---------------------------------------------------------------------------
// Convenience re-exports from the main entry point
// ---------------------------------------------------------------------------
export { z } from 'zod';
export { prompt } from '../mcp-server/prompts/utils/promptDefinition.js';
export { resource } from '../mcp-server/resources/utils/resourceDefinition.js';
export { disabledTool, tool, } from '../mcp-server/tools/utils/toolDefinition.js';
//# sourceMappingURL=app.js.map