/**
 * App Launcher
 *
 * Detects framework, starts dev server, and verifies app health.
 * Supports Next.js, Vite, Express, FastAPI, and more.
 *
 * @module scanners/runtime/app-launcher
 */
import { spawn } from "child_process";
import { readFile, access } from "fs/promises";
import { join } from "path";
import { logger } from "../../logger.js";
const DEFAULT_TIMEOUT = 60000;
const HEALTH_CHECK_INTERVAL = 1000;
const HEALTH_CHECK_MAX_RETRIES = 30;
/**
 * Framework detection patterns
 */
const FRAMEWORK_PATTERNS = [
    {
        framework: "nextjs",
        files: ["next.config.js", "next.config.mjs", "next.config.ts"],
        packageIndicators: ["next"],
        devCommand: "npm run dev",
        defaultPort: 3000,
        healthEndpoint: "/",
    },
    {
        framework: "vite",
        files: ["vite.config.js", "vite.config.ts", "vite.config.mjs"],
        packageIndicators: ["vite"],
        devCommand: "npm run dev",
        defaultPort: 5173,
        healthEndpoint: "/",
    },
    {
        framework: "create-react-app",
        files: [],
        packageIndicators: ["react-scripts"],
        devCommand: "npm start",
        defaultPort: 3000,
        healthEndpoint: "/",
    },
    {
        framework: "express",
        files: ["app.js", "server.js", "index.js"],
        packageIndicators: ["express"],
        devCommand: "npm run dev",
        defaultPort: 3000,
        healthEndpoint: "/health",
    },
    {
        framework: "fastapi",
        files: ["main.py", "app.py"],
        packageIndicators: ["fastapi", "uvicorn"],
        devCommand: "uvicorn main:app --reload",
        defaultPort: 8000,
        healthEndpoint: "/health",
    },
    {
        framework: "flask",
        files: ["app.py", "wsgi.py"],
        packageIndicators: ["flask"],
        devCommand: "flask run",
        defaultPort: 5000,
        healthEndpoint: "/health",
    },
    {
        framework: "django",
        files: ["manage.py"],
        packageIndicators: ["django"],
        devCommand: "python manage.py runserver",
        defaultPort: 8000,
        healthEndpoint: "/",
    },
    {
        framework: "rails",
        files: ["Gemfile", "config/routes.rb"],
        packageIndicators: ["rails"],
        devCommand: "rails server",
        defaultPort: 3000,
        healthEndpoint: "/",
    },
];
/**
 * Active app processes (for cleanup)
 */
const activeProcesses = new Map();
/**
 * Detect the framework used in a project
 */
export async function detectFramework(projectPath) {
    const indicators = [];
    let bestMatch;
    let bestConfidence = 0;
    for (const pattern of FRAMEWORK_PATTERNS) {
        let confidence = 0;
        // Check for framework-specific files
        for (const file of pattern.files) {
            try {
                await access(join(projectPath, file));
                confidence += 40;
                indicators.push(`Found ${file}`);
            }
            catch {
                // File doesn't exist
            }
        }
        // Check package.json for dependencies
        try {
            const packageJsonPath = join(projectPath, "package.json");
            const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
            const allDeps = {
                ...packageJson.dependencies,
                ...packageJson.devDependencies,
            };
            for (const indicator of pattern.packageIndicators) {
                if (allDeps[indicator]) {
                    confidence += 30;
                    indicators.push(`Found ${indicator} in package.json`);
                }
            }
            // Check for custom scripts
            if (packageJson.scripts?.dev) {
                confidence += 10;
            }
        }
        catch {
            // No package.json or parse error
        }
        // Check requirements.txt for Python projects
        if (pattern.framework === "fastapi" || pattern.framework === "flask") {
            try {
                const requirements = await readFile(join(projectPath, "requirements.txt"), "utf-8");
                for (const indicator of pattern.packageIndicators) {
                    if (requirements.toLowerCase().includes(indicator)) {
                        confidence += 30;
                        indicators.push(`Found ${indicator} in requirements.txt`);
                    }
                }
            }
            catch {
                // No requirements.txt
            }
        }
        if (confidence > bestConfidence) {
            bestConfidence = confidence;
            bestMatch = pattern;
        }
    }
    if (bestMatch && bestConfidence > 0) {
        // Try to get version from package.json
        let version;
        try {
            const packageJson = JSON.parse(await readFile(join(projectPath, "package.json"), "utf-8"));
            const allDeps = {
                ...packageJson.dependencies,
                ...packageJson.devDependencies,
            };
            for (const indicator of bestMatch.packageIndicators) {
                if (allDeps[indicator]) {
                    version = allDeps[indicator].replace(/[\^~]/, "");
                    break;
                }
            }
        }
        catch {
            // Ignore
        }
        return {
            framework: bestMatch.framework,
            version,
            confidence: Math.min(bestConfidence, 100),
            devCommand: bestMatch.devCommand,
            port: bestMatch.defaultPort,
            healthEndpoint: bestMatch.healthEndpoint,
            indicators,
        };
    }
    return {
        framework: "unknown",
        confidence: 0,
        devCommand: "npm run dev",
        port: 3000,
        healthEndpoint: "/",
        indicators: ["No framework detected"],
    };
}
/**
 * Check if a port is available
 */
async function isPortAvailable(port) {
    return new Promise((resolve) => {
        const net = require("net");
        const server = net.createServer();
        server.once("error", () => resolve(false));
        server.once("listening", () => {
            server.close();
            resolve(true);
        });
        server.listen(port);
    });
}
/**
 * Find an available port starting from the given port
 */
async function findAvailablePort(startPort) {
    let port = startPort;
    while (!(await isPortAvailable(port)) && port < startPort + 100) {
        port++;
    }
    return port;
}
/**
 * Wait for the app to be healthy
 */
async function waitForHealth(url, timeout) {
    const startTime = Date.now();
    const maxTime = startTime + timeout;
    let lastError;
    while (Date.now() < maxTime) {
        try {
            const checkStart = Date.now();
            const response = await fetch(url, {
                method: "GET",
                signal: AbortSignal.timeout(5000),
            });
            if (response.ok || response.status < 500) {
                return {
                    healthy: true,
                    responseTime: Date.now() - checkStart,
                };
            }
        }
        catch (error) {
            lastError = error;
        }
        await new Promise((resolve) => setTimeout(resolve, HEALTH_CHECK_INTERVAL));
    }
    logger.debug("runtime.health_check_failed", {
        url,
        error: lastError?.message,
    });
    return { healthy: false };
}
/**
 * Launch the app and wait for it to be ready
 */
export async function launchApp(config) {
    const startTime = Date.now();
    const { projectPath, timeout = DEFAULT_TIMEOUT } = config;
    // Detect framework if not specified
    const detection = config.framework
        ? { framework: config.framework, devCommand: "npm run dev", port: 3000, healthEndpoint: "/" }
        : await detectFramework(projectPath);
    const framework = detection.framework;
    const command = config.command || detection.devCommand;
    const requestedPort = config.port || detection.port;
    const healthEndpoint = config.healthEndpoint || detection.healthEndpoint;
    logger.info("runtime.launching_app", {
        framework,
        command,
        port: requestedPort,
    });
    // Find available port
    const port = await findAvailablePort(requestedPort);
    if (port !== requestedPort) {
        logger.info("runtime.port_changed", {
            requested: requestedPort,
            actual: port,
        });
    }
    // Build environment
    const env = {
        ...process.env,
        PORT: String(port),
        NODE_ENV: "development",
        ...config.env,
    };
    // Parse command
    const [cmd, ...args] = command.split(" ");
    // Start the process
    const child = spawn(cmd, args, {
        cwd: projectPath,
        env,
        stdio: ["ignore", "pipe", "pipe"],
        detached: false,
    });
    // Store for cleanup
    const processKey = `${projectPath}:${port}`;
    activeProcesses.set(processKey, child);
    // Capture output for debugging
    let stdout = "";
    let stderr = "";
    child.stdout?.on("data", (data) => {
        stdout += data.toString();
        // Log server ready messages
        const output = data.toString();
        if (output.includes("ready") ||
            output.includes("listening") ||
            output.includes("started")) {
            logger.debug("runtime.server_output", { output: output.trim() });
        }
    });
    child.stderr?.on("data", (data) => {
        stderr += data.toString();
    });
    // Handle process errors
    const processError = new Promise((resolve) => {
        child.on("error", (error) => {
            resolve({
                success: false,
                framework,
                port,
                url: `http://localhost:${port}`,
                healthStatus: "unhealthy",
                startupTime: Date.now() - startTime,
                error: `Process error: ${error.message}`,
            });
        });
        child.on("exit", (code) => {
            if (code !== 0 && code !== null) {
                resolve({
                    success: false,
                    framework,
                    port,
                    url: `http://localhost:${port}`,
                    healthStatus: "unhealthy",
                    startupTime: Date.now() - startTime,
                    error: `Process exited with code ${code}: ${stderr || stdout}`,
                });
            }
        });
    });
    // Wait for health check
    const url = `http://localhost:${port}`;
    const healthUrl = `${url}${healthEndpoint}`;
    const healthCheck = waitForHealth(healthUrl, timeout).then((result) => {
        if (result.healthy) {
            return {
                success: true,
                framework,
                port,
                pid: child.pid,
                url,
                healthStatus: "healthy",
                startupTime: Date.now() - startTime,
            };
        }
        else {
            return {
                success: false,
                framework,
                port,
                pid: child.pid,
                url,
                healthStatus: "timeout",
                startupTime: Date.now() - startTime,
                error: `Health check timed out after ${timeout}ms`,
            };
        }
    });
    // Race between health check and process error
    return Promise.race([healthCheck, processError]);
}
/**
 * Stop an app by project path or port
 */
export async function stopApp(projectPath, port) {
    // Find and kill the process
    for (const [key, child] of activeProcesses) {
        if (key.startsWith(projectPath) || (port && key.endsWith(`:${port}`))) {
            try {
                child.kill("SIGTERM");
                activeProcesses.delete(key);
                // Wait a bit for graceful shutdown
                await new Promise((resolve) => setTimeout(resolve, 1000));
                // Force kill if still running
                if (!child.killed) {
                    child.kill("SIGKILL");
                }
                logger.info("runtime.app_stopped", { key });
                return true;
            }
            catch (error) {
                logger.error("runtime.stop_error", {
                    key,
                    error: error.message,
                });
            }
        }
    }
    return false;
}
/**
 * Stop all running apps
 */
export async function stopAllApps() {
    for (const [key, child] of activeProcesses) {
        try {
            child.kill("SIGTERM");
            logger.info("runtime.app_stopped", { key });
        }
        catch {
            // Ignore errors
        }
    }
    activeProcesses.clear();
}
/**
 * Get list of running apps
 */
export function getRunningApps() {
    return Array.from(activeProcesses.entries()).map(([key, child]) => ({
        key,
        pid: child.pid,
    }));
}
//# sourceMappingURL=app-launcher.js.map