#!/usr/bin/env node
/**
 * Development server orchestrator for framework apps
 * Manages both backend and frontend processes with unified output
 */
import { spawn } from "child_process";
import fs from "fs";
import path from "path";
import net from "net";
import chalk from "chalk";
import boxen from "boxen";
import { displayStartupBanner } from "../utils/startupBanner.js";
import { createLogger } from "../core/logger.js";
const logger = createLogger("dev-server");
class DevServerOrchestrator {
    config;
    backendProcess = null;
    frontendProcess = null;
    startTime;
    isBackendReady = null;
    isFrontendReady = false;
    hasDetectedBackendReady = false;
    hasDetectedFrontendReady = false;
    hasShownBanner = false;
    retryCount = 0;
    outputBuffer = [];
    frontendError = null;
    constructor() {
        // Load config from package.json
        const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
        // Smart defaults based on common patterns
        const defaultBackendCommand = fs.existsSync("src/server/index.ts")
            ? "tsx watch src/server/index.ts"
            : fs.existsSync("src/index.ts")
                ? "tsx watch src/index.ts"
                : "tsx watch src/server.ts";
        const defaultFrontendCommand = fs.existsSync("web/package.json")
            ? "cd web && npm run dev"
            : "vite";
        // Get ports from environment or package.json or defaults
        const defaultBackendPort = process.env.PORT
            ? parseInt(process.env.PORT)
            : packageJson.devServer?.backendPort || 3000;
        this.config = {
            appName: packageJson.name || "SuperDangerous App",
            appVersion: packageJson.version || "0.0.0",
            packageName: packageJson.name,
            description: packageJson.description,
            backendPort: defaultBackendPort,
            frontendPort: packageJson.devServer?.frontendPort || 5173,
            webSocketPort: packageJson.devServer?.webSocketPort || defaultBackendPort,
            backendCommand: packageJson.devServer?.backendCommand || defaultBackendCommand,
            frontendCommand: packageJson.devServer?.frontendCommand || defaultFrontendCommand,
        };
        // Set process title for easier identification
        process.title = `dev-server:${this.config.appName}:${this.config.backendPort}`;
        this.startTime = Date.now();
    }
    showStartupBanner() {
        // Only print once both services are ready
        if (!this.isBackendReady || !this.isFrontendReady || this.hasShownBanner)
            return;
        logger.info("Clearing console for startup banner");
        console.clear();
        // Use the standardized banner from utils
        displayStartupBanner({
            appName: this.config.appName,
            appVersion: this.config.appVersion,
            packageName: this.config.packageName,
            description: this.config.description,
            port: this.config.backendPort,
            webPort: this.config.frontendPort,
            webSocketPort: this.config.webSocketPort,
            environment: process.env.NODE_ENV || "development",
            startTime: this.startTime,
        });
        this.hasShownBanner = true;
        // Discard buffered output — these are routine startup logs that
        // arrived before the banner. The banner itself signals readiness.
        this.outputBuffer = [];
    }
    parseCommand(command) {
        const parts = command.split(" ");
        return {
            cmd: parts[0],
            args: parts.slice(1),
        };
    }
    startBackend() {
        logger.info(chalk.gray("Starting backend on port " + this.config.backendPort + "..."));
        const { cmd, args } = this.parseCommand(this.config.backendCommand);
        this.backendProcess = spawn(cmd, args, {
            stdio: ["inherit", "pipe", "pipe"],
            shell: true,
            env: {
                ...process.env,
                LOG_LEVEL: "warn", // Only show warnings and errors
                NODE_ENV: "development",
                FORCE_COLOR: "3",
                PORT: this.config.backendPort.toString(),
                APP_NAME: this.config.appName,
                APP_PORT: this.config.backendPort.toString(),
            },
        });
        this.backendProcess.stdout?.on("data", (data) => {
            const output = data.toString();
            // Look for ANY indication the server is ready
            if (!this.hasDetectedBackendReady) {
                // Check for various ready indicators
                if (output.includes("Ready in") ||
                    output.includes("Server is running") ||
                    output.includes("Server started") ||
                    output.includes("Listening on") ||
                    output.includes(`${this.config.backendPort}`) ||
                    output.includes("✓")) {
                    this.hasDetectedBackendReady = true;
                    this.isBackendReady = true;
                    this.showStartupBanner();
                    return;
                }
            }
            // Check for port conflict and show enhanced error
            if (output.includes("EADDRINUSE") || output.includes("already in use")) {
                this.showPortConflictError(this.config.backendPort, "backend");
            }
            // Pass through backend stdout, but filter out the backend's startup banner
            // (the dev-server shows its own banner, so we don't want duplicates)
            // Banner lines typically contain box-drawing characters: ╭ ╰ │ ─
            const isBannerLine = /[╭╰│─╮╯]/.test(output) ||
                output.includes('Press Ctrl+C') ||
                /^\s*(v\d+\.\d+\.\d+|Environment:|Framework:)\s*$/.test(output.trim());
            if (output.trim() && !output.includes("ExperimentalWarning") && !isBannerLine) {
                if (this.hasShownBanner) {
                    process.stdout.write(output);
                }
                else {
                    // Buffer output until banner is shown
                    this.outputBuffer.push(output.trim());
                }
            }
        });
        this.backendProcess.stderr?.on("data", (data) => {
            const output = data.toString();
            // Check for port conflict
            if (output.includes("EADDRINUSE") ||
                output.includes("address already in use")) {
                this.showPortConflictError(this.config.backendPort, "backend");
            }
            // Pass through backend stderr as-is to preserve formatting
            // The backend logger already handles timestamps and coloring
            if (output.trim() && !output.includes("ExperimentalWarning")) {
                process.stderr.write(output);
            }
        });
        // Secondary readiness check: wait for port to open in case stdout is quiet
        this.waitForPort(this.config.backendPort, 8000).then((reachable) => {
            if (!this.hasDetectedBackendReady && reachable) {
                this.hasDetectedBackendReady = true;
                this.isBackendReady = true;
                this.showStartupBanner();
            }
        });
        this.backendProcess.on("error", (error) => {
            logger.error(chalk.red(`[Backend] Failed to start: ${error.message}`));
        });
        this.backendProcess.on("exit", (code) => {
            if (code !== 0 && code !== null) {
                this.isBackendReady = false;
                logger.error(chalk.red(`[Backend] Exited with code ${code}`));
            }
        });
        // Fallback: Mark as ready after timeout if we haven't detected it
        setTimeout(() => {
            if (!this.hasDetectedBackendReady) {
                if (this.isBackendReady !== false) {
                    logger.warn(chalk.yellow("Backend ready detection timeout - assuming ready"));
                    this.hasDetectedBackendReady = true;
                    this.isBackendReady = true;
                    this.showStartupBanner();
                }
                else {
                    logger.error(chalk.red("\nBackend failed to start. Check the error messages above."));
                    this.cleanup();
                    process.exit(1);
                }
            }
        }, 5000);
    }
    async startFrontend() {
        logger.info(chalk.gray("Starting frontend on port " + this.config.frontendPort + "..."));
        // Quick check if port is available before spawning process
        // This helps catch port conflicts faster than waiting for Vite to report them
        const portAvailable = await this.checkPort(this.config.frontendPort);
        if (!portAvailable) {
            this.frontendError = `Port ${this.config.frontendPort} is already in use`;
            // Don't call showPortConflictError here as it exits immediately
            // Instead, simulate a frontend exit to trigger the error banner
            setTimeout(() => {
                if (this.frontendProcess) {
                    this.frontendProcess.emit("exit", 1);
                }
                else {
                    // If no process created yet, show error directly
                    console.clear();
                    logger.error(boxen(chalk.red("⚠️  Frontend Failed to Start!\n\n") +
                        chalk.white(this.frontendError + "\n\n") +
                        chalk.gray("To fix this issue:\n") +
                        chalk.gray(`1. Stop the other process using port ${this.config.frontendPort}\n`) +
                        chalk.gray(`2. Check what's using it: ${chalk.cyan(`lsof -i :${this.config.frontendPort}`)}\n`) +
                        chalk.gray(`3. Kill it with: ${chalk.cyan(`kill -9 <PID>`)}\n`) +
                        chalk.gray(`4. Or use a different port in your configuration`), {
                        padding: 1,
                        margin: 1,
                        borderStyle: "round",
                        borderColor: "red",
                    }));
                    this.cleanup();
                    process.exit(1);
                }
            }, 100);
            return;
        }
        // Handle different frontend command formats
        let cmd;
        let args;
        if (this.config.frontendCommand.includes("cd ")) {
            // Complex command like "cd web && npm run dev"
            // Extract the directory and command
            const cdMatch = this.config.frontendCommand.match(/cd\s+([^\s&]+)\s*&&\s*(.+)/);
            if (cdMatch) {
                const [, dir, actualCmd] = cdMatch;
                // Run the command in the specified directory
                const parsed = this.parseCommand(actualCmd);
                cmd = parsed.cmd;
                args = parsed.args;
                // Override cwd below to run in the subdirectory
                this.frontendProcess = spawn(cmd, args, {
                    stdio: ["inherit", "pipe", "pipe"],
                    shell: true,
                    cwd: path.join(process.cwd(), dir), // Run in the subdirectory
                    env: {
                        ...process.env,
                        NODE_ENV: "development",
                        FORCE_COLOR: "3",
                    },
                });
            }
            else {
                // Fallback to sh -c for complex commands
                cmd = "sh";
                args = ["-c", this.config.frontendCommand];
                logger.info(chalk.gray(`Frontend command: ${cmd} ${args.join(" ")}`));
                this.frontendProcess = spawn(cmd, args, {
                    stdio: ["inherit", "pipe", "pipe"],
                    shell: true,
                    cwd: process.cwd(),
                    env: {
                        ...process.env,
                        NODE_ENV: "development",
                        FORCE_COLOR: "3",
                    },
                });
            }
        }
        else if (this.config.frontendCommand === "vite") {
            // Direct vite command
            cmd = "npx";
            args = ["vite", "--port", this.config.frontendPort.toString()];
        }
        else {
            // Parse other commands
            const parsed = this.parseCommand(this.config.frontendCommand);
            cmd = parsed.cmd;
            args = parsed.args;
        }
        // Only spawn if not already created above for cd commands
        if (!this.frontendProcess) {
            this.frontendProcess = spawn(cmd, args, {
                stdio: ["inherit", "pipe", "pipe"],
                shell: true,
                cwd: process.cwd(), // Explicitly set working directory
                env: {
                    ...process.env,
                    NODE_ENV: "development",
                    FORCE_COLOR: "3",
                },
            });
        }
        this.frontendProcess.stdout?.on("data", (data) => {
            const output = data.toString();
            // Debug: Log all frontend output
            if (process.env.DEBUG_DEV_SERVER) {
                logger.info(chalk.blue(`[Frontend stdout] ${output.trim()}`));
            }
            // Detect when frontend is ready
            if (!this.hasDetectedFrontendReady) {
                if (output.includes("ready in") ||
                    output.includes("Local:") ||
                    output.includes("➜") ||
                    output.includes(`${this.config.frontendPort}`)) {
                    // Extract actual port if different
                    const portMatch = output.match(/localhost:(\d+)/);
                    if (portMatch) {
                        this.config.frontendPort = parseInt(portMatch[1]);
                    }
                    this.hasDetectedFrontendReady = true;
                    this.isFrontendReady = true;
                    this.showStartupBanner();
                    return;
                }
            }
            // Check for port conflict
            if (output.includes("EADDRINUSE") ||
                output.includes("already in use") ||
                output.includes("Please stop the other process")) {
                // Store error for the exit handler
                this.frontendError = `Port ${this.config.frontendPort} is already in use`;
                logger.error(chalk.red(`\n⚠️  Port ${this.config.frontendPort} is already in use!`));
                logger.error(chalk.yellow("Please stop the other process or use a different port.\n"));
                this.cleanup();
                process.exit(1);
            }
            // Show errors
            if (output.toLowerCase().includes("error")) {
                const lines = output.trim().split("\n");
                lines.forEach((line) => {
                    if (line.trim() && !line.includes("➜")) {
                        // Strip existing ANSI codes to prevent corruption
                        // eslint-disable-next-line no-control-regex
                        const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
                        const msg = chalk.red(`[Frontend] ${cleanLine}`);
                        if (this.hasShownBanner) {
                            logger.error(msg);
                        }
                        else {
                            this.outputBuffer.push(msg);
                        }
                    }
                });
            }
        });
        this.frontendProcess.stderr?.on("data", (data) => {
            const output = data.toString().trim();
            if (output) {
                // Debug: Log all frontend stderr
                if (process.env.DEBUG_DEV_SERVER) {
                    logger.info(chalk.yellow(`[Frontend stderr] ${output}`));
                }
                // Check for Vite's port conflict error message
                // Vite outputs: "Error: Port XXXX is already in use"
                if (output.includes("Error:") && output.includes("is already in use")) {
                    // Extract port number from error message if possible
                    const portMatch = output.match(/Port (\d+)/);
                    const port = portMatch
                        ? parseInt(portMatch[1])
                        : this.config.frontendPort;
                    this.frontendError = `Port ${port} is already in use`;
                    // Don't call showPortConflictError immediately as Vite will exit
                    // The exit handler will show the proper error banner
                    return;
                }
                // Check for other port conflict formats
                if ((output.includes("Port") && output.includes("is in use")) ||
                    output.includes("EADDRINUSE")) {
                    // Store error for the exit handler
                    this.frontendError = `Port ${this.config.frontendPort} is already in use`;
                    // Try to show the error immediately, but Vite might exit first
                    this.showPortConflictError(this.config.frontendPort, "frontend");
                    return;
                }
                // Check if Vite is outputting its ready message to stderr
                if (!this.hasDetectedFrontendReady) {
                    if (output.includes("ready in") ||
                        output.includes("Local:") ||
                        output.includes("➜") ||
                        output.includes(`${this.config.frontendPort}`)) {
                        // Extract actual port if different
                        const portMatch = output.match(/localhost:(\d+)/);
                        if (portMatch) {
                            this.config.frontendPort = parseInt(portMatch[1]);
                        }
                        this.hasDetectedFrontendReady = true;
                        this.isFrontendReady = true;
                        this.showStartupBanner();
                        return;
                    }
                }
                // For important errors, show them immediately
                if (output.toLowerCase().includes("error") &&
                    !output.includes("ExperimentalWarning")) {
                    logger.error(chalk.red(`[Frontend] ${output}`));
                }
                else if (!this.hasShownBanner) {
                    // Buffer other output until banner is shown
                    this.outputBuffer.push(output);
                }
                else {
                    // After banner, pass through as-is
                    process.stderr.write(output + "\n");
                }
            }
        });
        this.frontendProcess.on("error", (error) => {
            // EAGAIN errors are temporary resource issues, retry
            if (error.code === "EAGAIN" && this.retryCount < 3) {
                this.retryCount++;
                logger.warn(chalk.yellow(`[Frontend] Resource temporarily unavailable, retrying (${this.retryCount}/3)...`));
                setTimeout(async () => await this.startFrontend(), 1000 * this.retryCount);
                return;
            }
            // For other errors, show in yellow (warning) not red (error) since it might still work
            logger.warn(chalk.yellow(`[Frontend] Process spawn warning: ${error.message}`));
            if (process.env.DEBUG_DEV_SERVER) {
                logger.info(chalk.gray(`[Frontend] Command: ${cmd} ${args.join(" ")}`));
                logger.info(chalk.gray(`[Frontend] Directory: ${process.cwd()}`));
            }
        });
        this.frontendProcess.on("exit", (code) => {
            if (code !== 0 && code !== null) {
                // Show specific error if we captured one
                if (this.frontendError) {
                    console.clear();
                    logger.error(boxen(chalk.red("⚠️  Frontend Failed to Start!\n\n") +
                        chalk.white(this.frontendError + "\n\n") +
                        chalk.gray("To fix this issue:\n") +
                        chalk.gray(`1. Stop the other process using port ${this.config.frontendPort}\n`) +
                        chalk.gray(`2. Check what's using it: ${chalk.cyan(`lsof -i :${this.config.frontendPort}`)}\n`) +
                        chalk.gray(`3. Kill it with: ${chalk.cyan(`kill -9 <PID>`)}\n`) +
                        chalk.gray(`4. Or use a different port in your configuration`), {
                        padding: 1,
                        margin: 1,
                        borderStyle: "round",
                        borderColor: "red",
                    }));
                }
                else {
                    logger.error(chalk.red(`[Frontend] Exited with code ${code}`));
                    if (!this.hasDetectedFrontendReady) {
                        logger.error(chalk.red(`[Frontend] Failed to start properly. Check that the frontend command works manually.`));
                    }
                }
                // Exit the dev-server if frontend fails to start
                if (!this.hasDetectedFrontendReady) {
                    this.cleanup();
                    process.exit(1);
                }
            }
        });
        // Fallback: Mark as ready after timeout if we haven't detected it
        setTimeout(() => {
            if (!this.hasDetectedFrontendReady) {
                logger.warn(chalk.yellow("Frontend ready detection timeout - assuming ready"));
                this.hasDetectedFrontendReady = true;
                this.isFrontendReady = true;
                this.showStartupBanner();
            }
        }, 5000);
    }
    killProcess(proc) {
        if (!proc || !proc.pid)
            return;
        const pid = proc.pid;
        try {
            proc.kill("SIGTERM");
        }
        catch (_e) {
            // Process might already be dead
        }
        // SIGKILL fallback after 500ms if the process is still alive
        setTimeout(() => {
            try {
                // Check if process is still running (signal 0 = existence check)
                process.kill(pid, 0);
                // Still alive — force kill
                process.kill(pid, "SIGKILL");
            }
            catch (_e) {
                // Already dead — nothing to do
            }
        }, 500);
    }
    cleanup() {
        this.killProcess(this.backendProcess);
        this.backendProcess = null;
        this.killProcess(this.frontendProcess);
        this.frontendProcess = null;
    }
    async checkPort(port) {
        return new Promise((resolve) => {
            const server = net.createServer();
            server.once("error", (err) => {
                if (err.code === "EADDRINUSE") {
                    resolve(false);
                }
                else {
                    resolve(true);
                }
            });
            server.once("listening", () => {
                server.close();
                resolve(true);
            });
            server.listen(port);
        });
    }
    async waitForPort(port, timeoutMs = 8000, host = "127.0.0.1") {
        const start = Date.now();
        return new Promise((resolve) => {
            const tryConnect = () => {
                const socket = net.connect({ port, host }, () => {
                    socket.end();
                    resolve(true);
                });
                socket.on("error", () => {
                    socket.destroy();
                    if (Date.now() - start >= timeoutMs) {
                        resolve(false);
                    }
                    else {
                        setTimeout(tryConnect, 200);
                    }
                });
            };
            tryConnect();
        });
    }
    async getProcessUsingPort(port) {
        try {
            const { execSync } = await import("child_process");
            // Get PID using the port
            const pidOutput = execSync(`lsof -i :${port} -P -t -sTCP:LISTEN 2>/dev/null || true`, { encoding: "utf-8" });
            const pid = pidOutput.trim().split("\n")[0]; // Get first PID if multiple
            if (!pid)
                return null;
            // Get full process command
            const commandOutput = execSync(`ps -p ${pid} -o command= 2>/dev/null || echo "unknown"`, { encoding: "utf-8" }).trim();
            // Simplify the command for display
            let command = commandOutput;
            if (command.includes("node")) {
                if (command.includes("tsx"))
                    command = "tsx (TypeScript)";
                else if (command.includes("vite"))
                    command = "vite (dev server)";
                else if (command.includes("nodemon"))
                    command = "nodemon";
                else
                    command = "node process";
            }
            return { pid, command };
        }
        catch {
            return null;
        }
    }
    async showPortConflictError(port, portType = "backend") {
        const processInfo = await this.getProcessUsingPort(port);
        console.clear();
        // Build the error message with subtle gray styling like the startup banner
        let lines = [];
        lines.push("");
        lines.push(`⚠️  ${chalk.yellow(portType.charAt(0).toUpperCase() + portType.slice(1))} port ${chalk.yellow(port)} is already in use!`);
        lines.push("");
        if (processInfo) {
            lines.push(chalk.gray(`Process: ${chalk.yellow(processInfo.command)}`));
            lines.push(chalk.gray(`PID: ${chalk.yellow(processInfo.pid)}`));
            lines.push("");
            lines.push(chalk.gray("🔧 To fix this, run:"));
            lines.push("");
            lines.push(chalk.cyan(`   kill -9 ${processInfo.pid}`));
            lines.push("");
        }
        else {
            lines.push(chalk.gray("Could not identify the process using this port."));
            lines.push("");
        }
        lines.push(chalk.gray("Other options:"));
        lines.push(chalk.gray(`• Check what's using it: ${chalk.cyan(`lsof -i :${port}`)}`));
        lines.push(chalk.gray(`• Kill all Node processes: ${chalk.cyan("pkill -f node")}`));
        lines.push(chalk.gray(`• Use a different port in your package.json`));
        lines.push("");
        // Use subtle gray box like the startup banner
        logger.error(boxen(lines.join("\n"), {
            padding: 1,
            margin: 1,
            borderStyle: "round",
            borderColor: "gray",
            dimBorder: true,
        }));
        this.cleanup();
        process.exit(1);
    }
    async start() {
        logger.info(chalk.cyan(`\nStarting ${this.config.appName} development server...\n`));
        // Check if backend port is available
        const backendAvailable = await this.checkPort(this.config.backendPort);
        if (!backendAvailable) {
            await this.showPortConflictError(this.config.backendPort, "backend");
            return;
        }
        // Check if frontend port is available
        const frontendAvailable = await this.checkPort(this.config.frontendPort);
        if (!frontendAvailable) {
            await this.showPortConflictError(this.config.frontendPort, "frontend");
            return;
        }
        // Start both processes
        this.startBackend();
        // Give backend a moment to start before frontend
        setTimeout(async () => await this.startFrontend(), 1000);
        // Handle graceful shutdown
        process.on("SIGINT", () => {
            logger.info(chalk.yellow("\n\nShutting down..."));
            this.cleanup();
            // Force exit after a delay if processes don't terminate
            setTimeout(() => process.exit(0), 1500);
        });
        process.on("SIGTERM", () => {
            this.cleanup();
            setTimeout(() => process.exit(0), 1500);
        });
        // Safety net: ensure child processes are cleaned up on any exit
        process.on("exit", () => {
            this.cleanup();
        });
    }
}
// Run the orchestrator
const orchestrator = new DevServerOrchestrator();
orchestrator.start();
//# sourceMappingURL=dev-server.js.map