/**
 * Streamable HTTP Transport for MCP
 *
 * Implements MCP Streamable HTTP transport (2025-03-26):
 * - HTTP POST for client->server messages
 * - SSE for server->client streaming
 * - Mcp-Session-Id header for session management
 *
 * CRITICAL: NEVER use console.log() - stdout is reserved for JSON-RPC protocol.
 *
 * @module server/transports/http-transport
 */
import http from 'http';
import { randomUUID } from 'crypto';
/** Maximum request body size (10MB). Requests exceeding this are rejected with 413. */
const MAX_BODY_SIZE = 10 * 1024 * 1024;
// ═══════════════════════════════════════════════════════════════════════════════
// HTTP TRANSPORT
// ═══════════════════════════════════════════════════════════════════════════════
export class HttpTransport {
    sessions = new Map();
    server = null;
    config;
    cleanupTimer = null;
    messageHandler = null;
    constructor(config) {
        this.config = {
            port: config?.port ?? (Number(process.env.MCP_HTTP_PORT) || 3100),
            sessionTtlMs: config?.sessionTtlMs ?? (Number(process.env.MCP_SESSION_TTL) || 3600) * 1000,
        };
    }
    /** Set the handler for incoming messages */
    onMessage(handler) {
        this.messageHandler = handler;
    }
    /** Start the HTTP server */
    async start() {
        // CORS origin: restrict to localhost by default. Set MCP_CORS_ORIGIN=* for open access.
        const corsOriginConfig = process.env.MCP_CORS_ORIGIN || '';
        this.server = http.createServer(async (req, res) => {
            // CORS headers — restricted to localhost origins by default (M-1)
            const requestOrigin = req.headers.origin || '';
            let allowedOrigin;
            if (corsOriginConfig === '*') {
                allowedOrigin = '*';
            }
            else if (corsOriginConfig) {
                allowedOrigin = requestOrigin === corsOriginConfig ? corsOriginConfig : '';
            }
            else {
                allowedOrigin = (requestOrigin === 'http://localhost' ||
                    requestOrigin === 'https://localhost' ||
                    requestOrigin.startsWith('http://localhost:') ||
                    requestOrigin.startsWith('https://localhost:') ||
                    requestOrigin === 'http://127.0.0.1' ||
                    requestOrigin === 'https://127.0.0.1' ||
                    requestOrigin.startsWith('http://127.0.0.1:') ||
                    requestOrigin.startsWith('https://127.0.0.1:')) ? requestOrigin : '';
            }
            res.setHeader('Access-Control-Allow-Origin', allowedOrigin || 'null');
            if (allowedOrigin && allowedOrigin !== '*') {
                res.setHeader('Vary', 'Origin');
            }
            res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
            res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
            res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
            if (req.method === 'OPTIONS') {
                res.writeHead(204);
                res.end();
                return;
            }
            try {
                if (req.method === 'POST' && req.url === '/mcp') {
                    await this.handlePost(req, res);
                }
                else if (req.method === 'GET' && req.url === '/mcp/sse') {
                    await this.handleSSE(req, res);
                }
                else if (req.method === 'GET' && req.url === '/health') {
                    const checks = {
                        transport: 'ok',
                        sessions: this.sessions.size,
                        handler: this.messageHandler ? 'ok' : 'error',
                    };
                    const status = checks.handler === 'ok' ? 'ok' : 'degraded';
                    const httpStatusCode = status === 'ok' ? 200 : 503;
                    res.writeHead(httpStatusCode, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ status, checks }));
                }
                else {
                    res.writeHead(404);
                    res.end('Not found');
                }
            }
            catch (error) {
                console.error('[HttpTransport] Request error:', error instanceof Error ? error.message : String(error));
                if (!res.headersSent) {
                    res.writeHead(500, { 'Content-Type': 'application/json' });
                    res.end(JSON.stringify({ error: 'Internal server error' }));
                }
            }
        });
        await new Promise((resolve, reject) => {
            this.server.listen(this.config.port, () => {
                console.error(`[HttpTransport] Listening on port ${this.config.port}`);
                resolve();
            });
            this.server.on('error', reject);
        });
        // Session cleanup every 60s
        this.cleanupTimer = setInterval(() => this.cleanupExpiredSessions(), 60000);
        this.cleanupTimer.unref();
    }
    /** Stop the HTTP server */
    async stop() {
        if (this.cleanupTimer) {
            clearInterval(this.cleanupTimer);
            this.cleanupTimer = null;
        }
        if (this.server) {
            await new Promise((resolve) => {
                this.server.close(() => resolve());
            });
            this.server = null;
        }
        this.sessions.clear();
        console.error('[HttpTransport] Stopped');
    }
    async handlePost(req, res) {
        // Get or create session
        let sessionId = req.headers['mcp-session-id'];
        if (sessionId && !this.sessions.has(sessionId)) {
            res.writeHead(400, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'Invalid session ID' }));
            return;
        }
        if (!sessionId) {
            sessionId = randomUUID();
            this.sessions.set(sessionId, {
                id: sessionId,
                createdAt: Date.now(),
                lastActivity: Date.now(),
                sseResponse: null,
            });
        }
        const session = this.sessions.get(sessionId);
        session.lastActivity = Date.now();
        // Read body with size limit (H-4)
        const body = await new Promise((resolve, reject) => {
            let data = '';
            let totalSize = 0;
            req.on('data', (chunk) => {
                totalSize += chunk.length;
                if (totalSize > MAX_BODY_SIZE) {
                    req.destroy();
                    reject(new Error('BODY_TOO_LARGE'));
                    return;
                }
                data += chunk;
            });
            req.on('end', () => resolve(data));
            req.on('error', reject);
        }).catch((err) => {
            if (err instanceof Error && err.message === 'BODY_TOO_LARGE') {
                res.writeHead(413, { 'Content-Type': 'application/json' });
                res.end(JSON.stringify({ error: 'Request body too large (max 10MB)' }));
                return null;
            }
            throw err;
        });
        if (body === null)
            return; // 413 already sent
        if (!this.messageHandler) {
            res.writeHead(500, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ error: 'No message handler registered' }));
            return;
        }
        const message = JSON.parse(body);
        const response = await this.messageHandler(sessionId, message);
        res.writeHead(200, {
            'Content-Type': 'application/json',
            'Mcp-Session-Id': sessionId,
        });
        res.end(JSON.stringify(response));
    }
    async handleSSE(req, res) {
        const sessionId = req.headers['mcp-session-id'];
        if (!sessionId || !this.sessions.has(sessionId)) {
            res.writeHead(400);
            res.end('Invalid session');
            return;
        }
        const session = this.sessions.get(sessionId);
        session.lastActivity = Date.now();
        session.sseResponse = res;
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive',
            'Mcp-Session-Id': sessionId,
        });
        // Keep-alive ping every 30s
        const keepAlive = setInterval(() => {
            try {
                res.write(': ping\n\n');
            }
            catch {
                clearInterval(keepAlive);
            }
        }, 30000);
        req.on('close', () => {
            clearInterval(keepAlive);
            if (session.sseResponse === res) {
                session.sseResponse = null;
            }
        });
    }
    /** Send SSE event to a session */
    sendEvent(sessionId, event, data) {
        const session = this.sessions.get(sessionId);
        if (session?.sseResponse) {
            try {
                session.sseResponse.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
            }
            catch (error) {
                console.error(`[HttpTransport] SSE send failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
                session.sseResponse = null;
            }
        }
    }
    cleanupExpiredSessions() {
        const now = Date.now();
        for (const [id, session] of this.sessions) {
            if (now - session.lastActivity > this.config.sessionTtlMs) {
                console.error(`[HttpTransport] Expiring session ${id}`);
                if (session.sseResponse) {
                    try {
                        session.sseResponse.end();
                    }
                    catch {
                        /* ignore close errors */
                    }
                }
                this.sessions.delete(id);
            }
        }
    }
    getSessionCount() {
        return this.sessions.size;
    }
}
//# sourceMappingURL=http-transport.js.map