import Fastify from 'fastify';
import { env, initializeNoPersistenceMode, isMemoryMode, memoryLogger, getBlockedWarnings, createLogger } from '@4runr/shared';
import { getPersistenceMode } from './config/persistence.js';
import { getRunStore } from './runs/index.js';
import { requireAuth } from './middleware/auth.js';
import { readRateLimit, writeRateLimit } from './middleware/rateLimit.js';
import { validateBody, validateQuery, validateParams } from './middleware/validate.js';
import { addCorrelationId, getCorrelationId } from './middleware/correlationId.js';
import { setSecurityHeaders, logSecurityValidation } from './middleware/security.js';
import { applyShieldConfigFromEnv, logShieldConfigValidation } from './security/shield-config.js';
import { applyRunKill, isTerminalRunStatus, setRunKillSideEffects } from './runs/run-kill.js';
import { createGatewayCancelAdapter } from './adapters/gateway-cancel-adapter.js';
import { publishSentinelPolicyViolationFireAndForget } from './adapters/redis-sentinel-publisher.js';
import { setCancelAdapter, setPolicyViolationPublisher } from '@4runr/sentinel';
import { errorHandler, handleValidationError, handleNotFound } from './middleware/errorHandler.js';
import { performHealthChecks, isAlive, computeReady } from './health/index.js';
import { createRunSchema, listRunsQuerySchema, runIdParamSchema, workspacePlanSchema, registryPublishSchema, privacyToggleSchema } from './schemas/runs.js';
import { disconnectPrisma } from './db/prisma.js';
import { getRedisClient, disconnectRedis, isRedisAvailable } from './db/redis.js';
import { createIdempotencyMiddleware, captureIdempotencyResponse, } from '../../../packages/middleware/idempotency/idempotency.js';
import { Sentinel } from '@4runr/sentinel';
import { Shield } from '@4runr/shield';
import { GatewayEventStream } from './adapters/sentinel-event-stream.js';
import { initializeQueue, queueRunExecution, shutdownQueue, getQueue, } from './queue/index.js';
import { setProcessorContext } from './queue/processor.js';
import { getMetrics, httpRequestDuration, httpRequestTotal, httpRequestErrors, runsCreated, runsStarted, runsCompleted, runsDuration, runsActive, sseConnectionsOpened, sseConnectionsClosed, sseActiveConnections, sseMessagesTotal, idempotencyHits, queueJobsQueued, queueJobsWaiting, queueJobsActive, healthCheckTotal, } from './metrics/index.js';
import { registerDevKitRoutes } from './devkit/routes.js';
import { mfaRoutes } from './routes/mfa.js';
import { gdprRoutes } from './routes/gdpr.js';
import { registerMonitoringRoutes } from './routes/monitoring.js';
import { ddosProtection } from './middleware/ddos-protection.js';
import { wrapLoggerForMonitoring } from './middleware/log-capture.js';
import { initializeDatabase, shutdownDatabase } from './db/init.js';
import { applyPersistedSentinelConfig } from './security/sentinel-config-store.js';
const projectRoot = process.cwd();
const persistenceMode = getPersistenceMode();
if (persistenceMode === 'memory') {
    initializeNoPersistenceMode(projectRoot);
}
let baseLogger = isMemoryMode ? memoryLogger : createLogger('Gateway');
baseLogger = wrapLoggerForMonitoring(baseLogger);
const PORT = env.PORT;
const HOST = env.HOST;
const isLocalBind = HOST === '127.0.0.1' || HOST === 'localhost';
let runStore = null;
const getStore = () => {
    if (!runStore) {
        runStore = getRunStore();
    }
    return runStore;
};
let dbClient = null;
const ttlSeconds = 24 * 60 * 60;
let idempotencyStore = null;
let idempotencyMiddleware = null;
(async function initializeIdempotencySync() {
    try {
        const { InMemoryIdempotencyStore } = await import('../../../packages/adapters/redis/IdempotencyStore.js');
        idempotencyStore = new InMemoryIdempotencyStore(ttlSeconds);
        idempotencyMiddleware = createIdempotencyMiddleware({
            enabled: true,
            ttlSeconds,
            store: idempotencyStore,
        });
        baseLogger.info('[IDEMPOTENCY] Middleware initialized synchronously');
    }
    catch (error) {
        const errorMsg = error instanceof Error ? error.message : String(error);
        const errorStack = error instanceof Error ? error.stack : undefined;
        baseLogger.error('[IDEMPOTENCY] Failed to initialize', {
            error: errorMsg,
            stack: errorStack
        });
    }
})();
async function initializeIdempotency() {
    try {
        const redisClient = await getRedisClient();
        if (redisClient) {
            const { RedisIdempotencyStore } = await import('../../../packages/adapters/redis/IdempotencyStore.js');
            const redisStore = new RedisIdempotencyStore(redisClient, ttlSeconds);
            idempotencyStore = redisStore;
            idempotencyMiddleware = createIdempotencyMiddleware({
                enabled: true,
                ttlSeconds,
                store: redisStore,
            });
            baseLogger.info('Idempotency store upgraded to Redis');
        }
        else {
            baseLogger.warn('Idempotency store using in-memory fallback (Redis not available)');
        }
    }
    catch (err) {
        baseLogger.error('Failed to initialize Redis idempotency store', { error: err instanceof Error ? err.message : String(err) });
        baseLogger.warn('Continuing with in-memory idempotency store');
    }
}
initializeIdempotency().catch(() => {
});
const fastify = Fastify({
    logger: false,
    trustProxy: true
});
fastify.addHook('onRequest', async (request, reply) => {
    await setSecurityHeaders(request, reply);
});
fastify.addHook('onRequest', async (request, reply) => {
    await addCorrelationId(request, reply);
    const correlationId = getCorrelationId(request);
    if (correlationId) {
        const requestLogger = wrapLoggerForMonitoring(createLogger('Gateway'));
        requestLogger.setCorrelationId(correlationId);
        request.logger = requestLogger;
    }
    else {
        request.logger = baseLogger;
    }
});
fastify.addHook('onRequest', async (request, reply) => {
    request.startTime = Date.now();
});
fastify.addHook('onResponse', async (request, reply) => {
    const startTime = request.startTime;
    if (startTime) {
        const duration = (Date.now() - startTime) / 1000;
        const method = request.method;
        const route = request.routeOptions?.url || request.url.split('?')[0] || 'unknown';
        const statusCode = reply.statusCode;
        httpRequestDuration.observe({ method, route, status_code: statusCode }, duration);
        httpRequestTotal.inc({ method, route, status_code: statusCode });
        if (statusCode >= 400) {
            httpRequestErrors.inc({ method, route, error_type: statusCode >= 500 ? 'server_error' : 'client_error' });
        }
    }
});
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, function (req, body, done) {
    try {
        if (body === '') {
            done(null, {});
        }
        else {
            const json = JSON.parse(body);
            done(null, json);
        }
    }
    catch (err) {
        const error = err;
        error.statusCode = 400;
        done(error, undefined);
    }
});
fastify.setErrorHandler((error, request, reply) => {
    if (error.validation) {
        handleValidationError(error, request, reply);
    }
    else {
        errorHandler(error, request, reply);
    }
});
fastify.setNotFoundHandler((request, reply) => {
    handleNotFound(request, reply);
});
baseLogger.info('Initializing database...');
const dbResult = await initializeDatabase(baseLogger);
if (dbResult.mode === 'memory') {
    baseLogger.warn('⚠️  Running in memory-only mode. Data will not persist across restarts.');
    if (dbResult.error) {
        baseLogger.warn(`Database initialization failed: ${dbResult.error}`);
    }
    if (dbResult.warnings) {
        dbResult.warnings.forEach(w => baseLogger.warn(`  • ${w}`));
    }
}
else {
    baseLogger.info(`✓ Database ready (${dbResult.mode} mode)`);
    dbClient = dbResult.client;
}
if (dbResult.client && persistenceMode !== 'memory') {
    baseLogger.info('Registering DevKit routes...');
    registerDevKitRoutes(fastify);
    baseLogger.info('✓ DevKit routes registered');
}
else {
    baseLogger.info('DevKit routes disabled (memory mode)');
}
fastify.get('/health', async (request, reply) => {
    const alive = isAlive();
    const { getMetricsSummary } = await import('./metrics/index.js');
    const summary = await getMetricsSummary();
    const healthChecks = await performHealthChecks();
    const statusCode = alive ? 200 : 503;
    reply.status(statusCode);
    healthCheckTotal.inc({ endpoint: '/health', status: String(statusCode) });
    return {
        ok: alive,
        status: alive ? 'alive' : 'dead',
        time: new Date().toISOString(),
        uptime: summary.uptime,
        memory: summary.memory,
        cpu: summary.cpu,
        persistence: persistenceMode,
        fourRunrDb: {
            mode: dbResult.mode,
            prismaConnected: !!dbClient,
        },
        auth: {
            enabled: env.AUTH_ENABLED,
            mode: env.AUTH_MODE
        },
        rateLimit: {
            enabled: env.RATE_LIMIT_ENABLED,
            max: env.RATE_LIMIT_MAX,
            window_ms: env.RATE_LIMIT_WINDOW_MS
        },
        checks: healthChecks.checks,
    };
});
fastify.get('/ready', async (request, reply) => {
    const health = await performHealthChecks();
    const ready = computeReady(health);
    const statusCode = ready ? 200 : 503;
    reply.status(statusCode);
    healthCheckTotal.inc({ endpoint: '/ready', status: String(statusCode) });
    return {
        ready,
        status: health.status,
        timestamp: new Date().toISOString(),
        checks: health.checks
    };
});
fastify.get('/liveness', async (request, reply) => {
    const alive = isAlive();
    const statusCode = alive ? 200 : 503;
    reply.status(statusCode);
    return {
        alive,
        timestamp: new Date().toISOString()
    };
});
fastify.get('/startup', async (request, reply) => {
    return {
        started: true,
        timestamp: new Date().toISOString()
    };
});
const sseConnections = new Map();
const sentinelEventStreams = new Map();
const sentinel = Sentinel.getInstance();
applyPersistedSentinelConfig(sentinel, baseLogger);
const shield = Shield.getInstance();
applyShieldConfigFromEnv(shield, env.SENTINEL_SHIELD_MODE);
let queueInitialized = false;
fastify.get('/metrics', async (request, reply) => {
    reply.header('X-Gateway', 'main');
    const activeConnections = Array.from(sseConnections.values()).reduce((sum, connections) => sum + connections.size, 0);
    sseActiveConnections.set(activeConnections);
    const runQueue = getQueue();
    if (runQueue) {
        try {
            const counts = await runQueue.getJobCounts('waiting', 'active', 'completed', 'failed', 'delayed', 'paused');
            const waiting = counts['waiting'] ?? 0;
            const activeJobs = counts['active'] ?? 0;
            queueJobsWaiting.set({ queue: 'run-execution' }, waiting);
            queueJobsActive.set({ queue: 'run-execution' }, activeJobs);
        }
        catch (err) {
            baseLogger.warn('Failed to refresh queue depth metrics', {
                error: err instanceof Error ? err.message : String(err),
            });
        }
    }
    const prometheusMetrics = await getMetrics();
    reply.type('text/plain');
    return prometheusMetrics;
});
fastify.get('/api/monitoring/summary', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    const { getMetricsSummary } = await import('./metrics/index.js');
    const summary = await getMetricsSummary();
    const dependencies = await performHealthChecks();
    const readyStatus = computeReady(dependencies);
    return {
        status: dependencies.status,
        alive: isAlive(),
        ready: readyStatus,
        uptime: summary.uptime,
        memory: summary.memory,
        cpu: summary.cpu,
        dependencies,
        timestamp: summary.timestamp,
        persistence: persistenceMode,
        database: {
            mode: dbResult.mode,
            connected: !!dbClient,
        },
    };
});
registerMonitoringRoutes(fastify, {
    requireAuth: isLocalBind ? false : requireAuth,
    readRateLimit: readRateLimit,
});
fastify.addHook('onRequest', ddosProtection);
fastify.post('/api/workspace/plan', {
    preHandler: [requireAuth, writeRateLimit, validateBody(workspacePlanSchema)],
}, async (request, reply) => {
    const body = request.body;
    const logger = request.logger || baseLogger;
    logger.info('Setting workspace plan', { prompt: body.prompt?.substring(0, 50) });
    return {
        success: true,
        prompt: body.prompt,
        updated_at: new Date().toISOString()
    };
});
const preHandlers = [
    requireAuth,
    writeRateLimit,
    validateBody(createRunSchema),
];
if (idempotencyMiddleware) {
    preHandlers.push(idempotencyMiddleware);
    baseLogger.info('[IDEMPOTENCY] Middleware added to /api/runs route');
}
else {
    baseLogger.warn('[IDEMPOTENCY] Middleware not available for /api/runs route');
}
try {
    const { aiProviderRoutes } = await import('./routes/ai-providers-simple.js');
    await aiProviderRoutes(fastify);
    baseLogger.info('AI Provider routes registered');
    Promise.resolve().then(async () => {
        try {
            const redisAvailable = await isRedisAvailable();
            if (!redisAvailable) {
                baseLogger.info('Redis not available, skipping credential store initialization');
                return;
            }
            const { getCredentialsStore } = await import('./ai-providers/redis-credentials-store.js');
            const credStore = getCredentialsStore();
            const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Redis credential load timed out after 5s')), 5000));
            await Promise.race([
                credStore.loadAllToEnv(),
                timeoutPromise
            ]);
            baseLogger.info('AI Provider credentials loaded from Redis');
        }
        catch (credError) {
            baseLogger.warn('Failed to load AI credentials from Redis (non-critical)', {
                error: credError instanceof Error ? credError.message : String(credError)
            });
        }
    }).catch((err) => {
        baseLogger.warn('Redis credential loading failed, continuing without stored credentials');
    });
}
catch (error) {
    baseLogger.warn('AI Provider routes not available', { error });
}
try {
    const { chatRoutes } = await import('./routes/chats.js');
    await chatRoutes(fastify);
    baseLogger.info('Chat history routes registered');
}
catch (error) {
    baseLogger.warn('Chat history routes not available', { error });
}
try {
    const { toolCredentialRoutes } = await import('./routes/tool-credentials.js');
    await toolCredentialRoutes(fastify);
    baseLogger.info('Tool credential routes registered (Layer 2 - Storage)');
}
catch (error) {
    baseLogger.warn('Tool credential routes not available', { error: error instanceof Error ? error.message : String(error) });
}
try {
    const { toolProxyRoutes } = await import('./routes/tool-proxy.js');
    await toolProxyRoutes(fastify);
    baseLogger.info('Tool proxy routes registered (Layer 2 - Execution)');
}
catch (error) {
    baseLogger.warn('Tool proxy routes not available', { error: error instanceof Error ? error.message : String(error) });
}
try {
    const { sentinelPolicyRoutes } = await import('./routes/sentinel-policies.js');
    await sentinelPolicyRoutes(fastify);
    baseLogger.info('Sentinel policy routes registered');
}
catch (error) {
    baseLogger.warn('Sentinel policy routes not available', { error: error instanceof Error ? error.message : String(error) });
}
try {
    const { sentinelPredictiveRoutes } = await import('./routes/sentinel-predictive.js');
    await sentinelPredictiveRoutes(fastify);
    baseLogger.info('Sentinel predictive routes registered');
}
catch (error) {
    baseLogger.warn('Sentinel predictive routes not available', { error: error instanceof Error ? error.message : String(error) });
}
try {
    baseLogger.info('Attempting to register Shield routes...');
    const { shieldRoutes } = await import('./routes/shield.js');
    baseLogger.info('Shield routes module imported successfully');
    await shieldRoutes(fastify);
    baseLogger.info('Shield routes registered successfully');
}
catch (error) {
    baseLogger.error('Shield routes registration failed', {
        error: error instanceof Error ? error.message : String(error),
        stack: error instanceof Error ? error.stack : undefined
    });
}
fastify.post('/api/runs', {
    preHandler: preHandlers,
    onSend: [captureIdempotencyResponse],
}, async (request, reply) => {
    const body = request.body;
    const logger = request.logger || baseLogger;
    if (body.clientToken && !request.headers['idempotency-key']) {
        const existingRun = await getStore().getRunByClientToken(body.clientToken);
        if (existingRun) {
            idempotencyHits.inc();
            reply.status(200);
            return {
                success: true,
                run: existingRun,
                idempotent: true
            };
        }
    }
    const runStartTime = Date.now();
    const run = await getStore().createRun({
        id: body.id,
        name: body.name || 'Test Run',
        input: body.input || null,
        clientToken: body.clientToken || null,
        tags: body.tags || []
    });
    runsCreated.inc({ status: run.status });
    runsActive.inc();
    logger.info('Run created', { runId: run.id, name: run.name });
    const { scheduleRunRetentionEnforcement } = await import('./runs/run-retention.js');
    scheduleRunRetentionEnforcement();
    const eventStream = new GatewayEventStream();
    sentinelEventStreams.set(run.id, eventStream);
    eventStream.emitCreated(run.id);
    const connections = sseConnections.get(run.id);
    if (connections) {
        connections.forEach(connection => {
            connection.write(`event: log\ndata: ${JSON.stringify({
                type: 'run_created',
                runId: run.id,
                timestamp: new Date().toISOString()
            })}\n\n`);
            sseMessagesTotal.inc();
        });
    }
    reply.status(201);
    return {
        success: true,
        run: run
    };
});
fastify.get('/api/runs', {
    preHandler: [requireAuth, readRateLimit, validateQuery(listRunsQuerySchema)],
}, async (request, reply) => {
    const query = request.query;
    const filter = {};
    if (query.status) {
        filter.status = query.status;
    }
    if (query.tag) {
        filter.tag = query.tag;
    }
    if (query.limit) {
        filter.limit = parseInt(query.limit, 10);
    }
    const runs = await getStore().listRuns(filter);
    return { runs };
});
fastify.get('/api/runs/:id', {
    preHandler: [requireAuth, readRateLimit, validateParams(runIdParamSchema)],
}, async (request, reply) => {
    const { id } = request.params;
    const run = await getStore().getRunById(id);
    if (!run) {
        return reply.status(404).send({ error: 'Run not found' });
    }
    return run;
});
fastify.post('/api/runs/:id/start', {
    preHandler: [requireAuth, writeRateLimit, validateParams(runIdParamSchema)],
}, async (request, reply) => {
    const { id } = request.params;
    const body = request.body;
    const run = await getStore().getRunById(id);
    if (!run) {
        return reply.status(404).send({ error: 'Run not found' });
    }
    if (run.status === 'running' || run.status === 'queued' || run.status === 'completed' || run.status === 'failed') {
        return reply.status(400).send({
            error: `Run is already ${run.status}`,
            run
        });
    }
    const logger = request.logger || baseLogger;
    const priority = body?.priority || 'normal';
    if (!['high', 'normal', 'low'].includes(priority)) {
        return reply.status(400).send({
            error: 'Invalid priority. Must be: high, normal, or low'
        });
    }
    const queue = getQueue();
    if (queue) {
        try {
            await getStore().updateRunStatus(id, 'queued', {
                startedAt: null
            });
            await queueRunExecution(id, priority);
            queueJobsQueued.inc({ queue: 'run-execution', priority });
            logger.info('Run queued for execution', { runId: id, priority });
            const updatedRun = await getStore().getRunById(id);
            const connections = sseConnections.get(id);
            if (connections) {
                connections.forEach(connection => {
                    connection.write(`event: log\ndata: ${JSON.stringify({
                        type: 'run_queued',
                        runId: id,
                        status: 'queued',
                        priority,
                        timestamp: new Date().toISOString()
                    })}\n\n`);
                    sseMessagesTotal.inc();
                });
            }
            return {
                success: true,
                run: updatedRun,
                queued: true
            };
        }
        catch (error) {
            logger.error('Failed to queue run execution', {
                runId: id,
                error: error instanceof Error ? error.message : String(error)
            });
            return reply.status(500).send({
                error: 'Failed to queue run execution',
                message: error instanceof Error ? error.message : String(error)
            });
        }
    }
    else {
        logger.warn('Queue not available, using synchronous execution', { runId: id });
        await getStore().updateRunStatus(id, 'running', {
            startedAt: new Date().toISOString()
        });
        runsStarted.inc();
        const updatedRun = await getStore().getRunById(id);
        const eventStream = sentinelEventStreams.get(id);
        if (eventStream) {
            if (sentinel.isEnabled()) {
                sentinel.watchRun(id, eventStream);
                logger.info('Sentinel watching run', { runId: id });
            }
            eventStream.emitStarted(id);
        }
        const connections = sseConnections.get(id);
        if (connections) {
            connections.forEach(connection => {
                connection.write(`event: log\ndata: ${JSON.stringify({
                    type: 'run_started',
                    runId: id,
                    status: 'running',
                    timestamp: new Date().toISOString()
                })}\n\n`);
                sseMessagesTotal.inc();
            });
        }
        const runStartTime = Date.now();
        setTimeout(async () => {
            const currentRun = await getStore().getRunById(id);
            if (currentRun && currentRun.status === 'running') {
                const runDuration = (Date.now() - runStartTime) / 1000;
                await getStore().updateRunStatus(id, 'completed', {
                    completedAt: new Date().toISOString(),
                    output: { result: 'Run completed successfully' },
                    logs: [
                        ...(currentRun.logs || []),
                        {
                            timestamp: new Date().toISOString(),
                            level: 'info',
                            message: 'Run completed'
                        }
                    ]
                });
                runsCompleted.inc({ status: 'completed' });
                runsDuration.observe({ status: 'completed' }, runDuration);
                runsActive.dec();
                const eventStream = sentinelEventStreams.get(id);
                if (eventStream) {
                    eventStream.emitFinish(id, 'ok');
                }
                sentinelEventStreams.delete(id);
                const connections = sseConnections.get(id);
                if (connections) {
                    connections.forEach(connection => {
                        connection.write(`event: end\ndata: ${JSON.stringify({
                            type: 'run_completed',
                            runId: id,
                            status: 'completed',
                            timestamp: new Date().toISOString()
                        })}\n\n`);
                        sseMessagesTotal.inc();
                    });
                }
            }
        }, 3000);
        logger.info('Run started (synchronous)', { runId: id });
        return {
            success: true,
            run: updatedRun,
            queued: false
        };
    }
});
fastify.post('/api/runs/:id/cancel', {
    preHandler: [requireAuth, writeRateLimit, validateParams(runIdParamSchema)],
}, async (request, reply) => {
    const { id } = request.params;
    const body = request.body;
    const run = await getStore().getRunById(id);
    if (!run) {
        return reply.status(404).send({ error: 'Run not found' });
    }
    const logger = request.logger || baseLogger;
    const reason = body.reason || 'manual_cancellation';
    if (isTerminalRunStatus(run.status)) {
        return {
            success: true,
            idempotent: true,
            runId: id,
            status: run.status,
            message: `Run already ${run.status}`,
        };
    }
    try {
        if (sentinel.isEnabled()) {
            sentinel.syncExternalKill(id, reason);
        }
        else {
            const eventStream = sentinelEventStreams.get(id);
            eventStream?.emitFinish(id, 'canceled');
        }
        const result = await applyRunKill(id, reason);
        return {
            success: true,
            idempotent: result.idempotent,
            message: result.idempotent ? 'Run already terminal' : 'Run canceled successfully',
            runId: id,
            status: result.status,
        };
    }
    catch (error) {
        logger.error('Failed to cancel run', {
            runId: id,
            error: error instanceof Error ? error.message : String(error)
        });
        return reply.status(500).send({
            error: 'Failed to cancel run',
            message: error instanceof Error ? error.message : String(error)
        });
    }
});
fastify.get('/api/runs/:id/logs/stream', {
    preHandler: [requireAuth, readRateLimit, validateParams(runIdParamSchema)],
}, async (request, reply) => {
    const { id } = request.params;
    const run = await getStore().getRunById(id);
    reply.raw.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*'
    });
    if (!sseConnections.has(id)) {
        sseConnections.set(id, new Set());
    }
    sseConnections.get(id).add(reply.raw);
    sseConnectionsOpened.inc();
    sseActiveConnections.inc();
    sseMessagesTotal.inc();
    let messageId = 1;
    const lastEventId = request.headers['last-event-id'];
    if (lastEventId && !isNaN(Number(lastEventId))) {
        messageId = Number(lastEventId) + 1;
    }
    const sendEvent = (data) => {
        reply.raw.write(`id: ${messageId++}\n`);
        reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
        sseMessagesTotal.inc();
    };
    sendEvent({
        type: 'connected',
        runId: id,
        timestamp: new Date().toISOString()
    });
    const heartbeatInterval = setInterval(() => {
        reply.raw.write(': keepalive\n\n');
    }, 15000);
    const statusInterval = setInterval(async () => {
        const currentRun = await getStore().getRunById(id);
        if (currentRun) {
            sendEvent({
                type: 'status_update',
                runId: id,
                status: currentRun.status,
                timestamp: new Date().toISOString()
            });
        }
    }, 5000);
    request.raw.on('close', () => {
        clearInterval(heartbeatInterval);
        clearInterval(statusInterval);
        const connections = sseConnections.get(id);
        if (connections) {
            connections.delete(reply.raw);
            sseConnectionsClosed.inc();
            sseActiveConnections.dec();
            if (connections.size === 0) {
                sseConnections.delete(id);
            }
        }
    });
});
fastify.get('/api/runs/logs/stream', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    reply.raw.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*'
    });
    const genericId = 's2-harness-generic';
    if (!sseConnections.has(genericId)) {
        sseConnections.set(genericId, new Set());
    }
    sseConnections.get(genericId).add(reply.raw);
    sseConnectionsOpened.inc();
    sseActiveConnections.inc();
    sseMessagesTotal.inc();
    let messageId = 1;
    const lastEventId = request.headers['last-event-id'];
    if (lastEventId && !isNaN(Number(lastEventId))) {
        messageId = Number(lastEventId) + 1;
    }
    const sendEvent = (data) => {
        reply.raw.write(`id: ${messageId++}\n`);
        reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
        sseMessagesTotal.inc();
    };
    sendEvent({
        type: 'connected',
        timestamp: new Date().toISOString()
    });
    const heartbeatInterval = setInterval(() => {
        reply.raw.write(': keepalive\n\n');
    }, 15000);
    request.raw.on('close', () => {
        clearInterval(heartbeatInterval);
        const connections = sseConnections.get(genericId);
        if (connections) {
            connections.delete(reply.raw);
            sseConnectionsClosed.inc();
            sseActiveConnections.dec();
            if (connections.size === 0) {
                sseConnections.delete(genericId);
            }
        }
    });
});
fastify.post('/api/registry/publish', {
    preHandler: [requireAuth, writeRateLimit, validateBody(registryPublishSchema)],
}, async (request, reply) => {
    const body = request.body;
    const logger = request.logger || baseLogger;
    logger.info('Registry publish', { name: body.name, version: body.version });
    reply.status(201);
    return {
        success: true,
        name: body.name,
        version: body.version,
        published_at: new Date().toISOString()
    };
});
fastify.get('/api/safety/check', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    return {
        status: 'safe',
        timestamp: new Date().toISOString()
    };
});
fastify.get('/sentinel/dashboard', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    const fs = await import('fs/promises');
    const path = await import('path');
    try {
        const dashboardPath = path.join(process.cwd(), 'apps/gateway/public/sentinel-dashboard.html');
        const html = await fs.readFile(dashboardPath, 'utf-8');
        return reply.type('text/html').send(html);
    }
    catch (error) {
        return reply.status(404).send({
            error: 'Dashboard not found',
            message: 'Sentinel dashboard HTML file not found'
        });
    }
});
fastify.get('/sentinel/health', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    const sentinelEnabled = env.SENTINEL_ENABLED;
    const sentinelHealthy = sentinel ? sentinel.isEnabled() : false;
    return {
        enabled: sentinelEnabled,
        healthy: sentinelHealthy,
        timestamp: new Date().toISOString(),
        status: sentinelHealthy ? 'healthy' : 'disabled',
        watchedRuns: sentinel ? sentinel.getWatchedRunIds().length : 0
    };
});
fastify.get('/sentinel/metrics', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    if (!sentinel) {
        return {
            error: 'Sentinel not initialized',
            timestamp: new Date().toISOString()
        };
    }
    const activeRuns = sentinel.getActiveRuns();
    const watchedRunIds = sentinel.getWatchedRunIds();
    let totalTokenUsage = 0;
    let totalCost = 0;
    let totalEvents = 0;
    const killReasons = {};
    const policyViolations = {};
    for (const runState of activeRuns) {
        totalTokenUsage += runState.tokenCount;
        totalCost += runState.cost || 0;
        totalEvents += runState.events.length;
        if (runState.killReason) {
            const reason = runState.killReason.split(':')[0];
            if (reason) {
                killReasons[reason] = (killReasons[reason] || 0) + 1;
            }
            const violations = sentinel.getPolicyViolations(runState.runId);
            for (const violation of violations) {
                policyViolations[violation.policy] = (policyViolations[violation.policy] || 0) + 1;
            }
        }
    }
    let totalLatency = 0;
    let latencyCount = 0;
    for (const runState of activeRuns) {
        if (runState.startedAt) {
            totalLatency += Date.now() - runState.startedAt;
            latencyCount++;
        }
    }
    const avgLatency = latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0;
    const allRuns = activeRuns.length;
    const config = sentinel.getConfig();
    return {
        enabled: config.enabled,
        healthy: true,
        activeRuns: allRuns,
        watchedRuns: watchedRunIds.length,
        totalEvents,
        totalTokenUsage,
        totalCost: parseFloat(totalCost.toFixed(4)),
        avgLatency,
        policyViolations,
        totalViolations: Object.values(policyViolations).reduce((sum, count) => sum + count, 0),
        killReasons,
        totalKills: Object.values(killReasons).reduce((sum, count) => sum + count, 0),
        config: config,
        totalSpans: 0,
        totalVerdicts: 0,
        flaggedHallucinations: 0,
        flaggedInjections: 0,
        flaggedPII: 0,
        timestamp: new Date().toISOString()
    };
});
fastify.post('/api/privacy/toggle', {
    preHandler: [requireAuth, writeRateLimit, validateBody(privacyToggleSchema)],
}, async (request, reply) => {
    const body = request.body;
    const logger = request.logger || baseLogger;
    logger.info('Privacy toggle', { storePlain: body.storePlain });
    return {
        success: true,
        storePlain: body.storePlain,
        updated_at: new Date().toISOString()
    };
});
fastify.post('/api/diagnostics/emit-demo-run', {
    ...(!isLocalBind && { preHandler: [requireAuth, writeRateLimit] }),
}, async (request, reply) => {
    const runId = `demo-run-${Date.now()}`;
    const logger = request.logger || baseLogger;
    logger.info('Demo run created', { runId });
    return {
        success: true,
        runId,
        message: 'Demo run created successfully'
    };
});
fastify.get('/diagnostics/sse-test', {
    ...(!isLocalBind && { preHandler: [requireAuth, readRateLimit] }),
}, async (request, reply) => {
    reply.raw.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*'
    });
    const sendEvent = (data) => {
        reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
    };
    const interval = setInterval(() => {
        sendEvent({
            type: 'test',
            timestamp: new Date().toISOString(),
            message: 'SSE test event'
        });
    }, 2000);
    request.raw.on('close', () => {
        clearInterval(interval);
    });
});
if (isMemoryMode) {
    fastify.get('/debug/logs', async (request, reply) => {
        const limit = parseInt(request.query?.limit || '50', 10);
        const logs = memoryLogger.getLogs(limit);
        return {
            logs,
            count: logs.length,
            mode: 'memory'
        };
    });
    fastify.get('/debug/blocked-writes', async (request, reply) => {
        const warnings = getBlockedWarnings();
        return {
            blockedWrites: warnings,
            count: warnings.length,
            mode: 'memory'
        };
    });
}
const start = async () => {
    try {
        logSecurityValidation();
        logShieldConfigValidation(shield, env.SENTINEL_SHIELD_MODE, env.NODE_ENV);
        setCancelAdapter(createGatewayCancelAdapter());
        setPolicyViolationPublisher((violation) => {
            publishSentinelPolicyViolationFireAndForget(violation.runId, {
                policy: violation.policy,
                reason: violation.reason,
                threshold: violation.threshold,
                actual: violation.actual,
                action: 'deny',
            });
        });
        setRunKillSideEffects({
            decRunsActive: () => runsActive.dec(),
            getSseConnections: (runId) => sseConnections.get(runId),
            onSseMessage: () => sseMessagesTotal.inc(),
            deleteEventStream: (runId) => { sentinelEventStreams.delete(runId); },
        });
        const queue = await initializeQueue();
        if (queue) {
            queueInitialized = true;
            setProcessorContext(sentinelEventStreams, sentinel, shield, sseConnections, sseMessagesTotal);
            baseLogger.info('Queue system initialized successfully');
        }
        else {
            baseLogger.warn('Queue system not available (Redis required). Using synchronous execution.');
        }
        if (persistenceMode === 'postgres') {
            Promise.resolve().then(async () => {
                try {
                    const { getPrismaClient } = await import('./db/prisma.js');
                    const prisma = await getPrismaClient();
                    await prisma.$connect();
                    baseLogger.info('Connected to PostgreSQL database');
                }
                catch (err) {
                    if (err?.code === 'MODULE_NOT_FOUND' || err?.message?.includes('Cannot find module')) {
                        baseLogger.warn('Prisma client not generated yet. Run: pnpm db:generate');
                        baseLogger.warn('Gateway will start in degraded mode until Prisma client is generated.');
                    }
                    else {
                        baseLogger.error('Failed to connect to PostgreSQL:', err?.message || err);
                        if (env.NODE_ENV === 'production') {
                            baseLogger.error('Production mode: database connection required');
                        }
                        else {
                            baseLogger.warn('Continuing in degraded mode (no database)');
                        }
                    }
                }
            }).catch((err) => {
                baseLogger.warn('Prisma initialization failed, continuing without database');
            });
        }
        baseLogger.info('=== STARTING MFA/GDPR ROUTE REGISTRATION ===');
        baseLogger.info('About to attempt MFA route registration...');
        try {
            try {
                await import('otplib');
                await import('qrcode');
            }
            catch (depError) {
                baseLogger.warn('MFA dependencies not available, skipping MFA routes', {
                    error: depError instanceof Error ? depError.message : String(depError)
                });
                throw new Error('MFA dependencies not installed');
            }
            baseLogger.info('Attempting to register MFA routes...');
            baseLogger.info('Calling mfaRoutes function...');
            await mfaRoutes(fastify);
            baseLogger.info('MFA routes registered successfully');
        }
        catch (error) {
            baseLogger.error('MFA routes registration failed', {
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined,
                name: error instanceof Error ? error.name : typeof error
            });
        }
        baseLogger.info('About to attempt GDPR route registration...');
        try {
            baseLogger.info('Attempting to register GDPR routes...');
            baseLogger.info('Calling gdprRoutes function...');
            await gdprRoutes(fastify);
            baseLogger.info('GDPR routes registered successfully');
        }
        catch (error) {
            baseLogger.error('GDPR routes registration failed', {
                error: error instanceof Error ? error.message : String(error),
                stack: error instanceof Error ? error.stack : undefined,
                name: error instanceof Error ? error.name : typeof error
            });
        }
        await fastify.listen({
            port: PORT,
            host: HOST
        });
        baseLogger.info(`Gateway server listening on ${HOST}:${PORT}`);
        baseLogger.info(`Persistence mode: ${persistenceMode}`);
        baseLogger.info(`Authentication: ${env.AUTH_ENABLED ? 'enabled' : 'disabled'}`);
        if (persistenceMode !== 'memory') {
            const { enforceRunRetention, getRunRetentionConfig } = await import('./runs/run-retention.js');
            const retentionCfg = getRunRetentionConfig();
            if (retentionCfg.enabled) {
                baseLogger.info('Run retention enabled', {
                    maxCount: retentionCfg.maxCount,
                    maxAgeDays: retentionCfg.maxAgeDays,
                });
                enforceRunRetention(retentionCfg).catch((err) => {
                    baseLogger.warn('Startup run retention failed', {
                        error: err instanceof Error ? err.message : String(err),
                    });
                });
            }
        }
        if (persistenceMode === 'memory') {
            baseLogger.warn('[Gateway] Running in NO-PERSISTENCE / MEMORY mode. All state is ephemeral.');
        }
        else if (persistenceMode === 'postgres') {
            baseLogger.info('[Gateway] PostgreSQL mode enabled. Generate Prisma client with: pnpm db:generate');
        }
    }
    catch (err) {
        baseLogger.error('Error starting server:', err);
        process.exit(1);
    }
};
const shutdown = async () => {
    baseLogger.info('Shutting down gateway...');
    try {
        baseLogger.info('Closing Fastify server...');
        await fastify.close();
        if (queueInitialized) {
            baseLogger.info('Shutting down queue...');
            await shutdownQueue();
        }
        if (sentinel) {
            baseLogger.info('Shutting down Sentinel...');
            sentinel.shutdown();
        }
        baseLogger.info('Disconnecting databases...');
        await shutdownDatabase(dbClient, baseLogger);
        await disconnectPrisma();
        await disconnectRedis();
        baseLogger.info('Gateway shut down successfully');
        process.exit(0);
    }
    catch (err) {
        baseLogger.error('Error during shutdown:', err);
        process.exit(1);
    }
};
process.on('uncaughtException', (err) => {
    baseLogger.error('Uncaught exception:', err);
    shutdown();
});
process.on('unhandledRejection', (reason, promise) => {
    baseLogger.error('Unhandled rejection:', { reason, promise });
    shutdown();
});
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
start();
//# sourceMappingURL=index.js.map