"use strict";
/**
 * Custom Next.js Server with WebSocket Support
 * Integrates WebSocket server for real-time communication
 * Issue #331: HTTPS support and auth cleanup on shutdown
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// IMPORTANT: Register uncaught exception handler FIRST, before any imports
// This ensures we catch WebSocket frame errors before other handlers
process.on('uncaughtException', (error) => {
    // Check for WebSocket-related errors that are non-fatal
    const isWebSocketError = error.code === 'WS_ERR_INVALID_UTF8' ||
        error.code === 'WS_ERR_INVALID_CLOSE_CODE' ||
        error.code === 'WS_ERR_UNEXPECTED_RSV_1' ||
        error.code === 'ECONNRESET' ||
        error.code === 'EPIPE' ||
        (error instanceof RangeError && error.message?.includes('Invalid WebSocket frame')) ||
        error.message?.includes('write after end');
    if (isWebSocketError) {
        // Silently ignore these non-fatal WebSocket frame errors
        // They commonly occur when mobile browsers send malformed close frames
        return;
    }
    // For other uncaught exceptions, log and exit
    console.error('Uncaught exception:', error);
    process.exit(1);
});
const http_1 = require("http");
const https_1 = require("https");
const fs_1 = require("fs");
const fs_2 = require("fs");
const url_1 = require("url");
const next_1 = __importDefault(require("next"));
const ws_server_1 = require("./src/lib/ws-server");
const worktrees_1 = require("./src/lib/git/worktrees");
const db_instance_1 = require("./src/lib/db-instance");
const response_poller_1 = require("./src/lib/polling/response-poller");
const auto_yes_manager_1 = require("./src/lib/polling/auto-yes-manager");
const schedule_manager_1 = require("./src/lib/schedule-manager");
const resource_cleanup_1 = require("./src/lib/resource-cleanup");
const db_migrations_1 = require("./src/lib/db-migrations");
const env_1 = require("./src/lib/env");
const db_repository_1 = require("./src/lib/db-repository");
const db_1 = require("./src/lib/db");
const session_cleanup_1 = require("./src/lib/session-cleanup");
const dev = process.env.NODE_ENV !== 'production';
const hostname = (0, env_1.getEnvByKey)('CM_BIND') || '127.0.0.1';
const port = parseInt((0, env_1.getEnvByKey)('CM_PORT') || '3000', 10);
// Issue #331: HTTPS configuration
const certPath = process.env.CM_HTTPS_CERT;
const keyPath = process.env.CM_HTTPS_KEY;
/** Maximum certificate file size: 1MB */
const MAX_CERT_FILE_SIZE = 1024 * 1024;
/**
 * Validate certificate file path for security
 * Issue #331: Certificate path validation
 */
function validateCertPath(filePath, label) {
    if (!(0, fs_1.existsSync)(filePath)) {
        console.error(`[Security] ${label} file not found: ${filePath}`);
        // ExitCode.CONFIG_ERROR = 2 (direct number, not imported from CLI types)
        process.exit(2);
    }
    try {
        (0, fs_1.accessSync)(filePath, fs_2.constants.R_OK);
    }
    catch {
        console.error(`[Security] ${label} file not readable: ${filePath}`);
        process.exit(2);
    }
    const realPath = (0, fs_1.realpathSync)(filePath);
    const stats = (0, fs_1.statSync)(realPath);
    if (stats.size > MAX_CERT_FILE_SIZE) {
        console.error(`[Security] ${label} file too large (${stats.size} bytes, max ${MAX_CERT_FILE_SIZE}): ${filePath}`);
        process.exit(2);
    }
    return realPath;
}
// Create Next.js app
const app = (0, next_1.default)({ dev, hostname, port });
const handle = app.getRequestHandler();
// Issue #331: Prevent Next.js (NextCustomServer) from registering its own upgrade
// event listener on the HTTP server.
//
// When next() is called without customServer:false, it creates a NextCustomServer
// instance. NextCustomServer.getRequestHandler() lazily calls setupWebSocketHandler()
// on the first HTTP request, which registers an upgrade listener that calls
// router-server's upgradeHandler → resolveRoutes({ res: socket }) → middleware match
// → serverResult.requestHandler(req, socket, parsedUrl). This passes the raw TCP
// socket as the HTTP response object, causing:
//   TypeError: Cannot read properties of undefined (reading 'bind')
// in handleRequestImpl when it tries to call _res.setHeader.bind(_res).
//
// Making setupWebSocketHandler a no-op prevents this listener from being added.
// All WebSocket upgrades are handled by ws-server.ts (our own upgrade listener).
app.setupWebSocketHandler = () => { };
app.prepare().then(() => {
    // Request handler for both HTTP and HTTPS
    const requestHandler = async (req, res) => {
        // Guard: res must be a proper HTTP ServerResponse (not a raw net.Socket).
        // Defense-in-depth: normally not needed after the setupWebSocketHandler fix above,
        // but kept for safety in case any other path passes a non-ServerResponse object.
        if (typeof res?.setHeader !== 'function') {
            return;
        }
        // Issue #332: Inject X-Real-IP header for IP restriction
        // [S3-005] This applies to HTTP requests only. WebSocket upgrade requests
        // are skipped below and handled by ws-server.ts (which uses socket.remoteAddress directly).
        const clientIp = req.socket.remoteAddress || '';
        if (process.env.CM_TRUST_PROXY !== 'true') {
            // CM_TRUST_PROXY=false: always overwrite to prevent forged headers
            req.headers['x-real-ip'] = clientIp;
        }
        else {
            // CM_TRUST_PROXY=true: only set if X-Forwarded-For is absent
            if (!req.headers['x-forwarded-for']) {
                req.headers['x-real-ip'] = clientIp;
            }
        }
        // Skip WebSocket upgrade requests - they are handled by the server 'upgrade' event.
        if (req.headers['upgrade']) {
            return;
        }
        const method = req.method ?? 'UNKNOWN';
        const url = req.url ?? '/';
        try {
            const parsedUrl = (0, url_1.parse)(url, true);
            await handle(req, res, parsedUrl);
        }
        catch (err) {
            console.error(`handleRequestImpl failed: ${method} ${url}`, err);
            if (!res.headersSent) {
                res.statusCode = 500;
                res.end('Internal Server Error');
            }
        }
    };
    // Issue #331: Create HTTP or HTTPS server
    let server;
    let protocol = 'http';
    if (certPath && keyPath) {
        const validatedCertPath = validateCertPath(certPath, 'Certificate');
        const validatedKeyPath = validateCertPath(keyPath, 'Key');
        try {
            const cert = (0, fs_1.readFileSync)(validatedCertPath);
            const key = (0, fs_1.readFileSync)(validatedKeyPath);
            server = (0, https_1.createServer)({ cert, key }, requestHandler);
            protocol = 'https';
            console.log('HTTPS server created with TLS certificates');
        }
        catch (error) {
            console.error('Failed to read TLS certificates:', error);
            process.exit(2);
        }
    }
    else {
        server = (0, http_1.createServer)(requestHandler);
    }
    // Setup WebSocket server
    (0, ws_server_1.setupWebSocket)(server);
    // Scan and sync worktrees on startup
    async function initializeWorktrees() {
        try {
            // Run database migrations first
            console.log('Running database migrations...');
            const db = (0, db_instance_1.getDbInstance)();
            (0, db_migrations_1.runMigrations)(db);
            // Get repository paths from environment variables
            const repositoryPaths = (0, worktrees_1.getRepositoryPaths)();
            // Issue #490: Also include DB-registered repositories (e.g. cloned repos)
            const dbRepositories = (0, db_repository_1.getAllRepositories)(db);
            const dbEnabledPaths = dbRepositories
                .filter(r => r.enabled)
                .map(r => r.path);
            // Merge env paths and DB-registered paths (deduplicate)
            const allPaths = [...new Set([...repositoryPaths, ...dbEnabledPaths])];
            if (allPaths.length === 0) {
                console.warn('Warning: No repository paths configured');
                console.warn('Set WORKTREE_REPOS (comma-separated) or MCBD_ROOT_DIR');
                return;
            }
            console.log(`Configured repositories: ${allPaths.length}`);
            allPaths.forEach((path, i) => {
                console.log(`  ${i + 1}. ${path}`);
            });
            // Issue #202: Register environment variable repositories and filter out excluded ones
            // registerAndFilterRepositories() encapsulates the ordering constraint:
            // registration MUST happen before filtering (see design policy Section 4)
            const { filteredPaths, excludedPaths, excludedCount } = (0, db_repository_1.registerAndFilterRepositories)(db, allPaths);
            if (excludedCount > 0) {
                console.log(`Excluded repositories: ${excludedCount}, Active repositories: ${filteredPaths.length}`);
                // SF-SEC-003: Log excluded repository paths for audit/troubleshooting
                excludedPaths.forEach(p => {
                    console.log(`  [excluded] ${p}`);
                });
                // Issue #202/#526: Remove worktrees of excluded repositories from DB
                // SF-002: cleanup -> delete order for excluded repositories
                // Sessions must be stopped before DB records are removed
                for (const excludedPath of excludedPaths) {
                    const resolvedPath = (0, db_repository_1.resolveRepositoryPath)(excludedPath);
                    const worktreeIds = (0, db_1.getWorktreeIdsByRepository)(db, resolvedPath);
                    if (worktreeIds.length > 0) {
                        // Issue #526: Clean up tmux sessions before deleting from DB
                        await (0, session_cleanup_1.cleanupMultipleWorktrees)(worktreeIds, session_cleanup_1.killWorktreeSession);
                        const result = (0, db_1.deleteWorktreesByIds)(db, worktreeIds);
                        console.log(`  Removed ${result.deletedCount} worktree(s) from excluded repository: ${resolvedPath}`);
                    }
                }
            }
            // Scan filtered repositories (excluded repos are skipped)
            const worktrees = await (0, worktrees_1.scanMultipleRepositories)(filteredPaths);
            // Issue #526: Sync to database with cleanup (MF-001)
            const { syncResult, cleanupWarnings } = await (0, session_cleanup_1.syncWorktreesAndCleanup)(db, worktrees);
            if (cleanupWarnings.length > 0) {
                console.warn(`Sync cleanup warnings: ${cleanupWarnings.join(', ')}`);
            }
            if (syncResult.deletedIds.length > 0) {
                console.log(`Cleaned up ${syncResult.deletedIds.length} deleted worktree(s)`);
            }
            console.log(`Total: ${worktrees.length} worktree(s) synced to database`);
        }
        catch (error) {
            console.error('Error initializing worktrees:', error);
        }
    }
    // H3 fix: Pass hostname to listen() so CM_BIND is respected.
    // Note: http.Server.listen(port, hostname, callback) does not pass err to callback;
    // listen errors emit an 'error' event instead.
    server.on('error', (err) => {
        if (err.code === 'EADDRINUSE') {
            console.error(`Port ${port} is already in use`);
        }
        else if (err.code === 'EADDRNOTAVAIL') {
            console.error(`Address ${hostname}:${port} is not available`);
        }
        else {
            console.error('Server error:', err);
        }
        process.exit(1);
    });
    server.listen(port, hostname, async () => {
        console.log(`> Ready on ${protocol}://${hostname}:${port}`);
        console.log(`> WebSocket server ready`);
        // Initialize worktrees after server starts
        await initializeWorktrees();
        // [S3-010] Initialize schedule manager AFTER worktrees are ready
        (0, schedule_manager_1.initScheduleManager)();
        // Issue #404: Initialize resource cleanup AFTER schedule manager
        (0, resource_cleanup_1.initResourceCleanup)();
    });
    // Graceful shutdown with timeout
    let isShuttingDown = false;
    function gracefulShutdown(signal) {
        if (isShuttingDown) {
            console.log('Shutdown already in progress, forcing exit...');
            process.exit(1);
        }
        isShuttingDown = true;
        console.log(`${signal} received: shutting down...`);
        // Stop polling first
        (0, response_poller_1.stopAllPolling)();
        // Issue #138: Stop all auto-yes pollers
        (0, auto_yes_manager_1.stopAllAutoYesPolling)();
        // Issue #294: Stop all scheduled executions (SIGKILL fire-and-forget)
        (0, schedule_manager_1.stopAllSchedules)();
        // Issue #404: Stop resource cleanup timer
        (0, resource_cleanup_1.stopResourceCleanup)();
        // Close WebSocket connections immediately (don't wait)
        (0, ws_server_1.closeWebSocket)();
        // Force exit after 3 seconds if graceful shutdown fails
        const forceExitTimeout = setTimeout(() => {
            console.log('Graceful shutdown timeout, forcing exit...');
            process.exit(1);
        }, 3000);
        // Try graceful HTTP server close
        server.close(() => {
            clearTimeout(forceExitTimeout);
            console.log('Server closed gracefully');
            process.exit(0);
        });
    }
    process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
    process.on('SIGINT', () => gracefulShutdown('SIGINT'));
});
