"use strict";
/**
 * CCS Config Dashboard - Web Server
 *
 * Express server with WebSocket support for real-time config management.
 * Single HTTP server handles REST API, static files, and WebSocket connections.
 * In dev mode, integrates Vite for HMR.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startServer = void 0;
const express_1 = __importDefault(require("express"));
const http_1 = __importDefault(require("http"));
const path_1 = __importDefault(require("path"));
const ws_1 = require("ws");
const websocket_1 = require("./websocket");
const auth_middleware_1 = require("./middleware/auth-middleware");
const request_logging_middleware_1 = require("./middleware/request-logging-middleware");
const sync_1 = require("../cliproxy/sync");
const aggregator_1 = require("./usage/aggregator");
const logging_1 = require("../services/logging");
const config_dashboard_host_1 = require("../commands/config-dashboard-host");
function getListenHost(options) {
    return options.host || config_dashboard_host_1.DEFAULT_DASHBOARD_HOST;
}
const logger = (0, logging_1.createLogger)('web-server');
/**
 * Start Express server with WebSocket support
 */
async function startServer(options) {
    const app = (0, express_1.default)();
    const server = http_1.default.createServer(app);
    const wss = new ws_1.WebSocketServer({
        noServer: true,
        maxPayload: 1024 * 1024, // 1MB hard limit to prevent DoS
        perMessageDeflate: false, // Prevent zip bomb attacks
    });
    // JSON body parsing with error handler for malformed JSON
    app.use(express_1.default.json());
    app.use((err, _req, res, next) => {
        if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
            res.status(400).json({ error: 'Invalid JSON in request body' });
            return;
        }
        next(err);
    });
    app.use(request_logging_middleware_1.requestLoggingMiddleware);
    // Session middleware (for dashboard auth)
    const sessionMiddleware = (0, auth_middleware_1.createSessionMiddleware)();
    app.use(sessionMiddleware);
    // Auth middleware (protects API routes when enabled)
    app.use(auth_middleware_1.authMiddleware);
    // CLIProxy local reverse proxy (avoids cross-origin issues in Docker)
    const cliproxyLocalProxy = (await Promise.resolve().then(() => __importStar(require('./routes/cliproxy-local-proxy')))).default;
    app.use('/api/cliproxy-local', cliproxyLocalProxy);
    // REST API routes (modularized)
    const { apiRoutes } = await Promise.resolve().then(() => __importStar(require('./routes/index')));
    app.use('/api', apiRoutes);
    // Shared data routes (Phase 07)
    const { sharedRoutes } = await Promise.resolve().then(() => __importStar(require('./shared-routes')));
    app.use('/api/shared', sharedRoutes);
    // Overview routes (Phase 07)
    const { overviewRoutes } = await Promise.resolve().then(() => __importStar(require('./overview-routes')));
    app.use('/api/overview', overviewRoutes);
    // Usage analytics routes
    const { usageRoutes } = await Promise.resolve().then(() => __importStar(require('./usage-routes')));
    app.use('/api/usage', usageRoutes);
    // Dev mode: use Vite middleware for HMR
    if (options.dev) {
        const { createServer: createViteServer } = await Promise.resolve().then(() => __importStar(require('vite')));
        const vite = await createViteServer({
            root: path_1.default.join(__dirname, '../../ui'),
            server: {
                middlewareMode: true,
                // Reuse the dashboard HTTP server for HMR in middleware mode.
                hmr: { server },
            },
            appType: 'spa',
        });
        app.use(vite.middlewares);
    }
    else {
        // Production: serve static files from dist/ui/
        const staticDir = options.staticDir || path_1.default.join(__dirname, '../ui');
        app.use(express_1.default.static(staticDir));
        // SPA fallback - return index.html for all non-API routes
        app.get('*', (_req, res) => {
            res.sendFile(path_1.default.join(staticDir, 'index.html'));
        });
    }
    server.on('upgrade', (request, socket, head) => {
        const pathname = getUpgradePathname(request.url);
        if (!pathname) {
            rejectWebSocketUpgrade(socket, 400, 'Invalid WebSocket upgrade request');
            return;
        }
        if (pathname !== '/ws') {
            if (!options.dev) {
                rejectWebSocketUpgrade(socket, 404, 'WebSocket endpoint not found');
            }
            return;
        }
        const response = new http_1.default.ServerResponse(request);
        sessionMiddleware(request, response, (error) => {
            if (error) {
                rejectWebSocketUpgrade(socket, 500, 'WebSocket session validation failed');
                return;
            }
            if (!(0, auth_middleware_1.isDashboardWebSocketUpgradeAllowed)(request)) {
                rejectWebSocketUpgrade(socket, (0, auth_middleware_1.getDashboardWebSocketRejectionStatus)(request), 'WebSocket access denied');
                return;
            }
            wss.handleUpgrade(request, socket, head, (ws) => {
                wss.emit('connection', ws, request);
            });
        });
    });
    // WebSocket connection handler + file watcher
    const { cleanup: wsCleanup } = (0, websocket_1.setupWebSocket)(wss);
    // Start auto-sync watcher (if enabled in config)
    (0, sync_1.startAutoSyncWatcher)();
    // Combined cleanup function
    const cleanup = () => {
        wsCleanup();
        (0, sync_1.stopAutoSyncWatcher)().catch(() => { });
        (0, aggregator_1.shutdownUsageAggregator)();
    };
    // Start listening
    return new Promise((resolve, reject) => {
        const listenHost = getListenHost(options);
        const onError = (error) => {
            logger.error('server.listen_failed', 'Dashboard server failed to start', {
                code: error.code || 'unknown',
                message: error.message,
                host: listenHost,
                port: options.port,
            });
            cleanup();
            reject(new Error(formatListenError(error, options)));
        };
        server.once('error', onError);
        const onListening = () => {
            server.off('error', onError);
            try {
                assertSafeDashboardBind(options, server.address());
            }
            catch (error) {
                cleanup();
                server.close(() => {
                    reject(error instanceof Error ? error : new Error(String(error)));
                });
                return;
            }
            logger.info('server.listening', 'Dashboard server listening', {
                host: listenHost,
                port: options.port,
                dev: Boolean(options.dev),
            });
            // Usage cache loads on-demand when Analytics page is visited
            // This keeps server startup instant for users who don't need analytics
            resolve({ server, wss, cleanup });
        };
        try {
            server.listen(options.port, listenHost, onListening);
        }
        catch (error) {
            server.off('error', onError);
            cleanup();
            reject(new Error(formatListenError(error, options)));
        }
    });
}
exports.startServer = startServer;
function getUpgradePathname(requestUrl) {
    try {
        return new URL(requestUrl ?? '/', 'http://localhost').pathname;
    }
    catch {
        return null;
    }
}
function rejectWebSocketUpgrade(socket, statusCode, message) {
    socket.write(`HTTP/1.1 ${statusCode} ${message}\r\n` +
        'Connection: close\r\n' +
        'Content-Type: text/plain; charset=utf-8\r\n' +
        `Content-Length: ${Buffer.byteLength(message)}\r\n` +
        '\r\n' +
        message);
    socket.destroy();
}
function assertSafeDashboardBind(options, address) {
    const listenHost = getListenHost(options);
    if (!(0, config_dashboard_host_1.isLoopbackHost)(listenHost) || typeof address === 'string' || !address) {
        return;
    }
    if ((0, config_dashboard_host_1.isLoopbackHost)(address.address)) {
        return;
    }
    throw new Error(`Dashboard host ${listenHost} resolved to non-loopback address ${address.address}; pass --host explicitly to allow network exposure.`);
}
function formatListenError(error, options) {
    const listenHost = getListenHost(options);
    if (error.code === 'EADDRINUSE') {
        return `Unable to bind ${listenHost}:${options.port}; the address may be unavailable or the port may already be in use`;
    }
    if (error.code === 'EADDRNOTAVAIL') {
        return `Cannot bind to ${listenHost}:${options.port} on this machine`;
    }
    if (error.code === 'EACCES') {
        return `Permission denied while binding to port ${options.port}`;
    }
    return `Cannot bind to ${listenHost}:${options.port}: ${error.message}`;
}
//# sourceMappingURL=index.js.map